Local inventory #16

Merged
Solvik merged 12 commits from feature/inventory into master 2019-08-26 16:54:49 +02:00
9 changed files with 439 additions and 1 deletions
Showing only changes of commit 823ecc6bf9 - Show all commits

View file

@ -18,7 +18,7 @@ MANUFACTURERS = {
def run(args): def run(args):
manufacturer = dmidecode.get_by_type('Chassis')[0].get('Manufacturer') manufacturer = dmidecode.get_by_type('Chassis')[0].get('Manufacturer')
server = MANUFACTURERS[manufacturer](dmidecode) server = MANUFACTURERS[manufacturer](dmi=dmidecode)
if args.debug: if args.debug:
server.print_debug() server.print_debug()
if args.register: if args.register:

240
netbox_agent/inventory.py Normal file
View file

@ -0,0 +1,240 @@
import subprocess
import re
from shutil import which
from netbox_agent.config import netbox_instance as nb
from netbox_agent.raid.hp import HPRaid
from netbox_agent.raid.dell import StorcliRaid
import netbox_agent.dmidecode as dmidecode
INVENTORY_TAG = {
'cpu': {'name': 'hw:cpu', 'slug': 'hw-cpu'},
'memory': {'name': 'hw:memory', 'slug': 'hw-memory'},
'disk': {'name': 'hw:disk', 'slug': 'hw-disk'},
'raid_card': {'name': 'hw:raid_card', 'slug': 'hw-raid-card'},
}
for key, tag in INVENTORY_TAG.items():
nb_tag = nb.extras.tags.get(
name=tag['name']
)
if not nb_tag:
nb_tag = nb.extras.tags.create(
name=tag['name'],
slug=tag['slug'],
comments=tag['name'],
)
def is_tool(name):
'''Check whether `name` is on PATH and marked as executable.'''
return which(name) is not None
class DictDiffer(object):
"""
Calculate the difference between two dictionaries as:
(1) items added
(2) items removed
(3) keys same in both but changed values
(4) keys same in both and unchanged values
"""
def __init__(self, current_dict, past_dict):
self.current_dict, self.past_dict = current_dict, past_dict
self.set_current, self.set_past = set(current_dict.keys()), set(past_dict.keys())
self.intersect = self.set_current.intersection(self.set_past)
def added(self):
return self.set_current - self.intersect
def removed(self):
return self.set_past - self.intersect
def changed(self):
return set(o for o in self.intersect if self.past_dict[o] != self.current_dict[o])
def unchanged(self):
return set(o for o in self.intersect if self.past_dict[o] == self.current_dict[o])
class Inventory():
"""
Better Inventory items coming, see:
- https://github.com/netbox-community/netbox/issues/3087
- https://github.com/netbox-community/netbox/issues/3333
"""
def __init__(self, server):
self.server = server
self.device_id = self.server.get_netbox_server().id
self.raid = None
self.disks = []
self.memories = []
def get_cpus(self):
model = None
nb = None
output = subprocess.getoutput('lscpu')
model_re = re.search(r'Model name: (.*)', output)
if len(model_re.groups()) > 0:
model = model_re.groups()[0].strip()
socket_re = re.search(r'Socket\(s\): (.*)', output)
if len(socket_re.groups()) > 0:
nb = int(socket_re.groups()[0].strip())
return nb, model
def create_netbox_cpus(self):
nb_cpus, model = self.get_cpus()
for i in range(nb_cpus):
cpu = nb.dcim.inventory_items.create(
device=self.device_id,
tags=[INVENTORY_TAG['cpu']['name']],
name=model,
discovered=True,
)
def get_raid_cards(self):
if self.server.manufacturer == 'Dell':
if is_tool('storcli'):
self.raid = StorcliRaid()
elif self.server.manufacturer == 'HP':
if is_tool('ssacli'):
self.raid = HPRaid()
if not self.raid:
return
controllers = self.raid.get_controllers()
if len(self.raid.get_controllers()):
return self.raid.get_controllers()
def get_netbox_raid_cards(self):
raid_cards = nb.dcim.inventory_items.filter(
device_id=self.device_id,
tag=INVENTORY_TAG['raid_card']['slug'],
)
return raid_cards
def find_or_create_manufacturer(self, name):
if name is None:
return none
manufacturer = nb.dcim.manufacturers.get(
name=name,
)
if not manufacturer:
manufacturer = nb.dcim.manufacturers.create(
name=name,
slug=name.lower(),
)
return manufacturer
def create_netbox_raid_card(self, raid_card):
manufacturer = self.find_or_create_manufacturer(
raid_card.get_manufacturer()
)
nb_raid_card = nb.dcim.inventory_items.create(
device=self.device_id,
discovered=True,
manufacturer=manufacturer.id,
tags=[INVENTORY_TAG['raid_card']['name']],
name='{}'.format(raid_card.get_product_name()),
serial='{}'.format(raid_card.get_serial_number()),
)
def create_netbox_raid_cards(self):
for raid_card in self.get_raid_cards():
self.create_netbox_raid_card(raid_card)
def update_netbox_raid_cards(self):
"""
Update raid cards in netbobx
Since we only push:
* Name
* Manufacturer
* Serial
We only need to handle destroy and new cards
"""
nb_raid_cards = self.get_netbox_raid_cards()
raid_cards = self.get_raid_cards()
# delete cards that are in netbox but not locally
# use the serial_number has the comparison element
for nb_raid_card in nb_raid_cards:
if nb_raid_card.serial not in [x.get_serial_number() for x in raid_cards]:
nb_raid_card.delete()
# create card that are not in netbox
for raid_card in raid_cards:
if raid_card.get_serial_number() not in [x.serial for x in nb_raid_cards]:
self.create_netbox_raid_card(raid_card)
def get_disks(self):
pass
def get_netbox_disks(self):
pass
def create_netbox_disks(self):
pass
def update_netbox_disks(self):
pass
def get_memory(self):
memories = []
for _, value in self.server.dmi.parse().items():
if value['DMIName'] == 'Memory Device' and \
value['Size'] != 'No Module Installed':
memories.append({
'Manufacturer': value['Manufacturer'].strip(),
'Size': value['Size'].strip(),
'PN': value['Part Number'].strip(),
'SN': value['Serial Number'].strip(),
'Locator': value['Locator'].strip(),
})
return memories
def get_memory_total_size(self):
total_size = 0
for memory in self.get_memory():
total_size += int(memory['Size'].split()[0])
return total_size
def get_netbox_memory(self):
memories = nb.dcim.inventory_items.filter(
device_id=self.device_id,
tag=INVENTORY_TAG['memory']['slug'],
)
return memories
def create_netbox_memory(self):
for memory in self.get_memory():
manufacturer = nb.dcim.manufacturers.get(
name=memory['Manufacturer']
)
if not manufacturer:
manufacturer = nb.dcim.manufacturers.create(
name=memory['Manufacturer'],
slug=memory['Manufacturer'].lower(),
)
memories = nb.dcim.inventory_items.create(
device=self.device_id,
discovered=True,
manufacturer=manufacturer.id,
tags=[INVENTORY_TAG['memory']['name']],
name='{} ({})'.format(memory['Locator'], memory['Size']),
part_id=memory['PN'],
serial=memory['SN'],
)
def update_netbox_memory(self):
pass
def create(self):
self.create_netbox_cpus()
self.create_netbox_memory()
self.create_netbox_raid_cards()
def update(self):
# assume we don't update CPU?
self.update_netbox_memory()
self.update_netbox_raid_cards()

View file

17
netbox_agent/raid/base.py Normal file
View file

@ -0,0 +1,17 @@
class RaidController():
def get_product_name(self):
raise NotImplemented
def get_serial_number(self):
raise NotImplemented
def get_manufacturer(self):
raise NotImplemented
def get_firmware_version(self):
raise NotImplemented
class Raid():
def get_controllers(self):
raise NotImplemented

38
netbox_agent/raid/dell.py Normal file
View file

@ -0,0 +1,38 @@
import subprocess
import json
from netbox_agent.raid.base import Raid, RaidController
class StorcliController(RaidController):
def __init__(self, data):
self.data = data
def get_product_name(self):
return self.data['Product Name']
def get_manufacturer(self):
return None
def get_serial_number(self):
return self.data['Serial Number']
def get_firmware_version(self):
return self.data['FW Package Build']
class StorcliRaid(Raid):
def __init__(self):
self.output = subprocess.getoutput('storcli /call show J')
self.data = json.loads(self.output)
self.controllers = []
if len([
x for x in self.data['Controllers'] \
if x['Command Status']['Status'] == 'Success'
]) > 0:
for controller in self.data['Controllers']:
self.controllers.append(
StorcliController(controller['Response Data'])
)
def get_controllers(self):
return self.controllers

126
netbox_agent/raid/hp.py Normal file
View file

@ -0,0 +1,126 @@
import re
import subprocess
from netbox_agent.raid.base import Raid, RaidController
REGEXP_CONTROLLER_HP = re.compile(r'Smart Array ([a-zA-Z0-9- ]+) in Slot ([0-9]+)')
def _get_indentation(string):
"""Return the number of spaces before the current line."""
return len(string) - len(string.lstrip(' '))
def _get_key_value(string):
"""Return the (key, value) as a tuple from a string."""
# Normally all properties look like this:
# Unique Identifier: 600508B1001CE4ACF473EE9C826230FF
# Disk Name: /dev/sda
# Mount Points: None
key = ''
value = ''
try:
key, value = string.split(':')
except ValueError:
# This handles the case when the property of a logical drive
# returned is as follows. Here we cannot split by ':' because
# the disk id has colon in it. So if this is about disk,
# then strip it accordingly.
# Mirror Group 0: physicaldrive 6I:1:5
string = string.lstrip(' ')
if string.startswith('physicaldrive'):
fields = string.split(' ')
key = fields[0]
value = fields[1]
else:
# TODO(rameshg87): Check if this ever occurs.
return None, None
return key.lstrip(' ').rstrip(' '), value.lstrip(' ').rstrip(' ')
def _get_dict(lines, start_index, indentation):
"""Recursive function for parsing hpssacli/ssacli output."""
info = {}
current_item = None
i = start_index
while i < len(lines):
current_line = lines[i]
if current_line.startswith('Note:'):
i = i + 1
continue
current_line_indentation = _get_indentation(current_line)
if current_line_indentation == indentation:
current_item = current_line.lstrip(' ')
info[current_item] = {}
i = i + 1
continue
if i >= len(lines) - 1:
key, value = _get_key_value(current_line)
# If this is some unparsable information, then
# just skip it.
if key:
info[current_item][key] = value
return info, i
next_line = lines[i + 1]
next_line_indentation = _get_indentation(next_line)
if current_line_indentation == next_line_indentation:
key, value = _get_key_value(current_line)
if key:
info[current_item][key] = value
i = i + 1
elif next_line_indentation > current_line_indentation:
ret_dict, j = _get_dict(lines, i, current_line_indentation)
info[current_item].update(ret_dict)
i = j + 1
elif next_line_indentation < current_line_indentation:
key, value = _get_key_value(current_line)
if key:
info[current_item][key] = value
return info, i
return info, i
class HPRaidController(RaidController):
def __init__(self, controller_name, data):
self.controller_name = controller_name
self.data = data
def get_product_name(self):
return self.controller_name
def get_manufacturer(self):
return 'HP'
def get_serial_number(self):
return self.data['Serial Number']
def get_firmware_version(self):
return self.data['Firmware Version']
class HPRaid(Raid):
def __init__(self):
self.output = subprocess.getoutput('ssacli ctrl all show detail')
self.controllers = []
self.convert_to_dict()
def convert_to_dict(self):
lines = self.output.split('\n')
lines = list(filter(None, lines))
j = -1
while j < len(lines):
info_dict, j = _get_dict(lines, j + 1, 0)
if len(info_dict.keys()):
_product_name = list(info_dict.keys())[0]
product_name = REGEXP_CONTROLLER_HP.search(_product_name)
if product_name:
self.controllers.append(
HPRaidController(product_name.group(1), info_dict[_product_name])
)
def get_controllers(self):
return self.controllers

View file

@ -5,6 +5,7 @@ import socket
from netbox_agent.config import netbox_instance as nb from netbox_agent.config import netbox_instance as nb
import netbox_agent.dmidecode as dmidecode import netbox_agent.dmidecode as dmidecode
from netbox_agent.location import Datacenter, Rack from netbox_agent.location import Datacenter, Rack
from netbox_agent.inventory import Inventory
from netbox_agent.network import Network from netbox_agent.network import Network
@ -204,6 +205,8 @@ class ServerBase():
self.network = Network(server=self) self.network = Network(server=self)
self.network.create_netbox_network_cards() self.network.create_netbox_network_cards()
self.inventory = Inventory(server=self)
self.inventory.create()
logging.debug('Server created!') logging.debug('Server created!')
def _netbox_update_chassis_for_blade(self, server, datacenter): def _netbox_update_chassis_for_blade(self, server, datacenter):
@ -271,12 +274,21 @@ class ServerBase():
# check network cards # check network cards
self.network = Network(server=self) self.network = Network(server=self)
self.network.update_netbox_network_cards() self.network.update_netbox_network_cards()
# check raid_cards
self.inventory = Inventory(server=self)
self.inventory.update()
if update: if update:
server.save() server.save()
logging.debug('Finished updating Server!') logging.debug('Finished updating Server!')
def print_debug(self): def print_debug(self):
# FIXME: do something more generic by looping on every get_* methods # FIXME: do something more generic by looping on every get_* methods
print(self.inventory.get_memory())
print(self.inventory.get_raid_cards())
print(self.inventory.get_netbox_raid_cards())
# print(self.inventory.get_netbox_memory())
# print(self.inventory.update_netbox_memory())
return
print('Datacenter:', self.get_datacenter()) print('Datacenter:', self.get_datacenter())
print('Netbox Datacenter:', self.get_netbox_datacenter()) print('Netbox Datacenter:', self.get_netbox_datacenter())
print('Rack:', self.get_rack()) print('Rack:', self.get_rack())

View file

@ -2,6 +2,10 @@ from netbox_agent.server import ServerBase
class DellHost(ServerBase): class DellHost(ServerBase):
def __init__(self, *args, **kwargs):
super(DellHost, self).__init__(*args, **kwargs)
self.manufacturer = 'Dell'
def is_blade(self): def is_blade(self):
return self.get_product_name().startswith('PowerEdge M') return self.get_product_name().startswith('PowerEdge M')

View file

@ -6,6 +6,7 @@ class HPHost(ServerBase):
super(HPHost, self).__init__(*args, **kwargs) super(HPHost, self).__init__(*args, **kwargs)
if self.is_blade(): if self.is_blade():
self.hp_rack_locator = self._find_rack_locator() self.hp_rack_locator = self._find_rack_locator()
self.manufacturer = 'HP'
def is_blade(self): def is_blade(self):
return self.get_product_name().startswith('ProLiant BL') return self.get_product_name().startswith('ProLiant BL')