merge
This commit is contained in:
commit
1d00696671
22 changed files with 970 additions and 554 deletions
|
|
@ -1,5 +1,7 @@
|
|||
from packaging import version
|
||||
import netbox_agent.dmidecode as dmidecode
|
||||
from netbox_agent.config import config
|
||||
from netbox_agent.config import netbox_instance as nb
|
||||
from netbox_agent.logging import logging # NOQA
|
||||
from netbox_agent.vendors.dell import DellHost
|
||||
from netbox_agent.vendors.generic import GenericHost
|
||||
|
|
@ -32,11 +34,15 @@ def run(config):
|
|||
except KeyError:
|
||||
server = GenericHost(dmi=dmi)
|
||||
|
||||
if version.parse(nb.version) < version.parse('2.9'):
|
||||
print('netbox-agent is not compatible with Netbox prior to verison 2.9')
|
||||
return False
|
||||
|
||||
if config.register or config.update_all or config.update_network or \
|
||||
config.update_location or config.update_inventory or config.update_psu:
|
||||
server.netbox_create_or_update(config)
|
||||
if config.debug:
|
||||
server.print_debug()
|
||||
if config.register or config.update_all or config.update_network or config.update_location or \
|
||||
config.update_inventory or config.update_psu:
|
||||
server.netbox_create_or_update(config)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,13 @@ def get_config():
|
|||
p.add_argument('--update-inventory', action='store_true', help='Update inventory')
|
||||
p.add_argument('--update-location', action='store_true', help='Update location')
|
||||
p.add_argument('--update-psu', action='store_true', help='Update PSU')
|
||||
p.add_argument('--purge-old-devices', action='store_true',
|
||||
help='Purge existing (old ?) devices having same name but different serial')
|
||||
p.add_argument('--expansion-as-device', action='store_true',
|
||||
help='Manage blade expansions as external devices')
|
||||
|
||||
p.add_argument('--log_level', default='debug')
|
||||
p.add_argument('--netbox.ssl_ca_certs_file', help='SSL CA certificates file')
|
||||
p.add_argument('--netbox.url', help='Netbox URL')
|
||||
p.add_argument('--netbox.token', help='Netbox API Token')
|
||||
p.add_argument('--netbox.ssl_verify', default=True, action='store_true',
|
||||
|
|
@ -39,6 +44,8 @@ def get_config():
|
|||
help="Command to output hostname, used as Device's name in netbox")
|
||||
p.add_argument('--device.tags', default=r'',
|
||||
help='tags to use for a host')
|
||||
p.add_argument('--device.custom_fields', default=r'',
|
||||
help='custom_fields to use for a host, eg: field1=v1,field2=v2')
|
||||
p.add_argument('--device.blade_role', default=r'Blade',
|
||||
help='role to use for a blade server')
|
||||
p.add_argument('--device.chassis_role', default=r'Server Chassis',
|
||||
|
|
@ -70,13 +77,22 @@ def get_config():
|
|||
p.add_argument('--network.lldp', help='Enable auto-cabling feature through LLDP infos')
|
||||
p.add_argument('--inventory', action='store_true',
|
||||
help='Enable HW inventory (CPU, Memory, RAID Cards, Disks) feature')
|
||||
p.add_argument('--process-virtual-drives', action='store_true',
|
||||
help='Process virtual drives information from RAID '
|
||||
'controllers to fill disk custom_fields')
|
||||
p.add_argument('--force-disk-refresh', action='store_true',
|
||||
help='Forces disks detection reprocessing')
|
||||
p.add_argument('--dump-disks-map',
|
||||
help='File path to dump physical/virtual disks map')
|
||||
|
||||
options = p.parse_args()
|
||||
return options
|
||||
|
||||
|
||||
config = get_config()
|
||||
|
||||
|
||||
def get_netbox_instance():
|
||||
config = get_config()
|
||||
if config.netbox.url is None or config.netbox.token is None:
|
||||
logging.error('Netbox URL and token are mandatory')
|
||||
sys.exit(1)
|
||||
|
|
@ -85,7 +101,12 @@ def get_netbox_instance():
|
|||
url=get_config().netbox.url,
|
||||
token=get_config().netbox.token,
|
||||
)
|
||||
if get_config().netbox.ssl_verify is False:
|
||||
ca_certs_file = config.netbox.ssl_ca_certs_file
|
||||
if ca_certs_file is not None:
|
||||
session = requests.Session()
|
||||
session.verify = ca_certs_file
|
||||
nb.http_session = session
|
||||
elif config.netbox.ssl_verify is False:
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
session = requests.Session()
|
||||
session.verify = False
|
||||
|
|
@ -94,5 +115,4 @@ def get_netbox_instance():
|
|||
return nb
|
||||
|
||||
|
||||
config = get_config()
|
||||
netbox_instance = get_netbox_instance()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
import logging
|
||||
import re
|
||||
|
||||
import pynetbox
|
||||
|
||||
from netbox_agent.config import config
|
||||
from netbox_agent.config import netbox_instance as nb
|
||||
from netbox_agent.lshw import LSHW
|
||||
|
|
@ -10,6 +5,12 @@ from netbox_agent.misc import get_vendor, is_tool
|
|||
from netbox_agent.raid.hp import HPRaid
|
||||
from netbox_agent.raid.omreport import OmreportRaid
|
||||
from netbox_agent.raid.storcli import StorcliRaid
|
||||
import traceback
|
||||
import pynetbox
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
INVENTORY_TAG = {
|
||||
'cpu': {'name': 'hw:cpu', 'slug': 'hw-cpu'},
|
||||
|
|
@ -46,10 +47,11 @@ class Inventory():
|
|||
- no scan of NVMe devices
|
||||
"""
|
||||
|
||||
def __init__(self, server):
|
||||
def __init__(self, server, update_expansion=False):
|
||||
self.create_netbox_tags()
|
||||
self.server = server
|
||||
netbox_server = self.server.get_netbox_server()
|
||||
self.update_expansion = update_expansion
|
||||
netbox_server = self.server.get_netbox_server(update_expansion)
|
||||
|
||||
self.device_id = netbox_server.id if netbox_server else None
|
||||
self.raid = None
|
||||
|
|
@ -58,6 +60,7 @@ class Inventory():
|
|||
self.lshw = LSHW()
|
||||
|
||||
def create_netbox_tags(self):
|
||||
ret = []
|
||||
for key, tag in INVENTORY_TAG.items():
|
||||
nb_tag = nb.extras.tags.get(
|
||||
name=tag['name']
|
||||
|
|
@ -68,6 +71,8 @@ class Inventory():
|
|||
slug=tag['slug'],
|
||||
comments=tag['name'],
|
||||
)
|
||||
ret.append(nb_tag)
|
||||
return ret
|
||||
|
||||
def find_or_create_manufacturer(self, name):
|
||||
if name is None:
|
||||
|
|
@ -97,7 +102,7 @@ class Inventory():
|
|||
logging.info('Tag {tag} is missing, returning empty array.'.format(tag=tag))
|
||||
items = []
|
||||
|
||||
return items
|
||||
return list(items)
|
||||
|
||||
def create_netbox_inventory_item(self, device_id, tags, vendor, name, serial, description):
|
||||
manufacturer = self.find_or_create_manufacturer(vendor)
|
||||
|
|
@ -152,7 +157,7 @@ class Inventory():
|
|||
if motherboard.get('serial') not in [x.serial for x in nb_motherboards]:
|
||||
self.create_netbox_inventory_item(
|
||||
device_id=self.device_id,
|
||||
tags=[INVENTORY_TAG['motherboard']['name']],
|
||||
tags=[{'name': INVENTORY_TAG['motherboard']['name']}],
|
||||
vendor='{}'.format(motherboard.get('vendor', 'N/A')),
|
||||
serial='{}'.format(motherboard.get('serial', 'No SN')),
|
||||
name='{}'.format(motherboard.get('name')),
|
||||
|
|
@ -165,7 +170,7 @@ class Inventory():
|
|||
device=self.device_id,
|
||||
manufacturer=manufacturer.id,
|
||||
discovered=True,
|
||||
tags=[INVENTORY_TAG['interface']['name']],
|
||||
tags=[{'name': INVENTORY_TAG['interface']['name']}],
|
||||
name="{}".format(iface['product']),
|
||||
serial='{}'.format(iface['serial']),
|
||||
description='{} {}'.format(iface['description'], iface['name'])
|
||||
|
|
@ -198,7 +203,7 @@ class Inventory():
|
|||
device=self.device_id,
|
||||
manufacturer=manufacturer.id,
|
||||
discovered=True,
|
||||
tags=[INVENTORY_TAG['cpu']['name']],
|
||||
tags=[{'name': INVENTORY_TAG['cpu']['name']}],
|
||||
name=cpu['product'],
|
||||
description='CPU {}'.format(cpu['location']),
|
||||
# asset_tag=cpu['location']
|
||||
|
|
@ -220,9 +225,9 @@ class Inventory():
|
|||
|
||||
self.create_netbox_cpus()
|
||||
|
||||
def get_raid_cards(self):
|
||||
def get_raid_cards(self, filter_cards=False):
|
||||
raid_class = None
|
||||
if self.server.manufacturer == 'Dell':
|
||||
if self.server.manufacturer in ('Dell', 'Huawei'):
|
||||
if is_tool('omreport'):
|
||||
raid_class = OmreportRaid
|
||||
if is_tool('storcli'):
|
||||
|
|
@ -235,9 +240,15 @@ class Inventory():
|
|||
return []
|
||||
|
||||
self.raid = raid_class()
|
||||
controllers = self.raid.get_controllers()
|
||||
if len(self.raid.get_controllers()):
|
||||
return controllers
|
||||
|
||||
if filter_cards and config.expansion_as_device \
|
||||
and self.server.own_expansion_slot():
|
||||
return [
|
||||
c for c in self.raid.get_controllers()
|
||||
if c.is_external() is self.update_expansion
|
||||
]
|
||||
else:
|
||||
return self.raid.get_controllers()
|
||||
|
||||
def create_netbox_raid_card(self, raid_card):
|
||||
manufacturer = self.find_or_create_manufacturer(
|
||||
|
|
@ -250,7 +261,7 @@ class Inventory():
|
|||
device=self.device_id,
|
||||
discovered=True,
|
||||
manufacturer=manufacturer.id if manufacturer else None,
|
||||
tags=[INVENTORY_TAG['raid_card']['name']],
|
||||
tags=[{'name': INVENTORY_TAG['raid_card']['name']}],
|
||||
name='{}'.format(name),
|
||||
serial='{}'.format(serial),
|
||||
description='RAID Card',
|
||||
|
|
@ -276,7 +287,7 @@ class Inventory():
|
|||
device_id=self.device_id,
|
||||
tag=[INVENTORY_TAG['raid_card']['slug']]
|
||||
)
|
||||
raid_cards = self.get_raid_cards()
|
||||
raid_cards = self.get_raid_cards(filter_cards=True)
|
||||
|
||||
# delete cards that are in netbox but not locally
|
||||
# use the serial_number has the comparison element
|
||||
|
|
@ -292,53 +303,60 @@ class Inventory():
|
|||
if raid_card.get_serial_number() not in [x.serial for x in nb_raid_cards]:
|
||||
self.create_netbox_raid_card(raid_card)
|
||||
|
||||
def is_virtual_disk(self, disk):
|
||||
def is_virtual_disk(self, disk, raid_devices):
|
||||
disk_type = disk.get('type')
|
||||
logicalname = disk.get('logicalname')
|
||||
description = disk.get('description')
|
||||
size = disk.get('size')
|
||||
product = disk.get('product')
|
||||
|
||||
if logicalname in raid_devices or disk_type is None:
|
||||
return True
|
||||
non_raid_disks = [
|
||||
'MR9361-8i',
|
||||
]
|
||||
|
||||
if size is None and logicalname is None or \
|
||||
'virtual' in product.lower() or 'logical' in product.lower() or \
|
||||
if logicalname in raid_devices or \
|
||||
disk_type is None or \
|
||||
product in non_raid_disks or \
|
||||
'virtual' in product.lower() or \
|
||||
'logical' in product.lower() or \
|
||||
'volume' in description.lower() or \
|
||||
description == 'SCSI Enclosure' or \
|
||||
'volume' in description.lower():
|
||||
(size is None and logicalname is None):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_hw_disks(self):
|
||||
disks = []
|
||||
|
||||
for raid_card in self.get_raid_cards(filter_cards=True):
|
||||
disks.extend(raid_card.get_physical_disks())
|
||||
|
||||
raid_devices = [
|
||||
d.get('custom_fields', {}).get('vd_device')
|
||||
for d in disks
|
||||
if d.get('custom_fields', {}).get('vd_device')
|
||||
]
|
||||
|
||||
for disk in self.lshw.get_hw_linux("storage"):
|
||||
if self.is_virtual_disk(disk):
|
||||
if self.is_virtual_disk(disk, raid_devices):
|
||||
continue
|
||||
|
||||
logicalname = disk.get('logicalname')
|
||||
description = disk.get('description')
|
||||
size = disk.get('size', 0)
|
||||
product = disk.get('product')
|
||||
serial = disk.get('serial')
|
||||
|
||||
d = {}
|
||||
d["name"] = ""
|
||||
d['Size'] = '{} GB'.format(int(size / 1024 / 1024 / 1024))
|
||||
d['logicalname'] = logicalname
|
||||
d['description'] = description
|
||||
d['SN'] = serial
|
||||
d['Model'] = product
|
||||
size =int(disk.get('size', 0)) / 1073741824
|
||||
d = {
|
||||
"name": "",
|
||||
'Size': '{} GB'.format(size),
|
||||
'logicalname': disk.get('logicalname'),
|
||||
'description': disk.get('description'),
|
||||
'SN': disk.get('serial'),
|
||||
'Model': disk.get('product'),
|
||||
'Type': disk.get('type'),
|
||||
}
|
||||
if disk.get('vendor'):
|
||||
d['Vendor'] = disk['vendor']
|
||||
else:
|
||||
d['Vendor'] = get_vendor(disk['product'])
|
||||
disks.append(d)
|
||||
|
||||
for raid_card in self.get_raid_cards():
|
||||
disks += raid_card.get_physical_disks()
|
||||
|
||||
# remove duplicate serials
|
||||
seen = set()
|
||||
uniq = [x for x in disks if x['SN'] not in seen and not seen.add(x['SN'])]
|
||||
|
|
@ -351,53 +369,79 @@ class Inventory():
|
|||
|
||||
logicalname = disk.get('logicalname')
|
||||
desc = disk.get('description')
|
||||
# nonraid disk
|
||||
if logicalname and desc:
|
||||
if type(logicalname) is list:
|
||||
logicalname = logicalname[0]
|
||||
name = '{} - {} ({})'.format(
|
||||
desc,
|
||||
logicalname,
|
||||
disk.get('Size', 0))
|
||||
description = 'Device {}'.format(disk.get('logicalname', 'Unknown'))
|
||||
else:
|
||||
name = '{} ({})'.format(disk['Model'], disk['Size'])
|
||||
description = '{}'.format(disk['Type'])
|
||||
name = '{} ({})'.format(disk['Model'], disk['Size'])
|
||||
description = disk['Type']
|
||||
|
||||
_ = nb.dcim.inventory_items.create(
|
||||
device=self.device_id,
|
||||
discovered=True,
|
||||
tags=[INVENTORY_TAG['disk']['name']],
|
||||
name=name,
|
||||
serial=disk['SN'],
|
||||
part_id=disk['Model'],
|
||||
description=description,
|
||||
manufacturer=manufacturer.id if manufacturer else None
|
||||
)
|
||||
parms = {
|
||||
'device': self.device_id,
|
||||
'discovered': True,
|
||||
'tags': [{'name': INVENTORY_TAG['disk']['name']}],
|
||||
'name': name,
|
||||
'serial': disk['SN'],
|
||||
'part_id': disk['Model'],
|
||||
'description': description,
|
||||
'manufacturer': getattr(manufacturer, "id", None),
|
||||
}
|
||||
if config.process_virtual_drives:
|
||||
parms['custom_fields'] = disk.get("custom_fields", {})
|
||||
|
||||
_ = nb.dcim.inventory_items.create(**parms)
|
||||
|
||||
logging.info('Creating Disk {model} {serial}'.format(
|
||||
model=disk['Model'],
|
||||
serial=disk['SN'],
|
||||
))
|
||||
|
||||
def dump_disks_map(self, disks):
|
||||
disk_map = [d['custom_fields'] for d in disks if 'custom_fields' in d]
|
||||
if config.dump_disks_map == "-":
|
||||
f = sys.stdout
|
||||
else:
|
||||
f = open(config.dump_disks_map, "w")
|
||||
f.write(
|
||||
json.dumps(
|
||||
disk_map,
|
||||
separators=(',', ':'),
|
||||
indent=4,
|
||||
sort_keys=True
|
||||
)
|
||||
)
|
||||
if config.dump_disks_map != "-":
|
||||
f.close()
|
||||
|
||||
def do_netbox_disks(self):
|
||||
nb_disks = self.get_netbox_inventory(
|
||||
device_id=self.device_id,
|
||||
tag=INVENTORY_TAG['disk']['slug'])
|
||||
tag=INVENTORY_TAG['disk']['slug']
|
||||
)
|
||||
disks = self.get_hw_disks()
|
||||
if config.dump_disks_map:
|
||||
try:
|
||||
self.dump_disks_map(disks)
|
||||
except Exception as e:
|
||||
logging.error("Failed to dump disks map: {}".format(e))
|
||||
logging.debug(traceback.format_exc())
|
||||
disk_serials = [d['SN'] for d in disks if 'SN' in d]
|
||||
|
||||
# delete disks that are in netbox but not locally
|
||||
# use the serial_number has the comparison element
|
||||
for nb_disk in nb_disks:
|
||||
if nb_disk.serial not in [x['SN'] for x in disks if x.get('SN')]:
|
||||
if nb_disk.serial not in disk_serials or \
|
||||
config.force_disk_refresh:
|
||||
logging.info('Deleting unknown locally Disk {serial}'.format(
|
||||
serial=nb_disk.serial,
|
||||
))
|
||||
nb_disk.delete()
|
||||
|
||||
if config.force_disk_refresh:
|
||||
nb_disks = self.get_netbox_inventory(
|
||||
device_id=self.device_id,
|
||||
tag=INVENTORY_TAG['disk']['slug']
|
||||
)
|
||||
|
||||
# create disks that are not in netbox
|
||||
for disk in disks:
|
||||
if disk.get('SN') not in [x.serial for x in nb_disks]:
|
||||
if disk.get('SN') not in [d.serial for d in nb_disks]:
|
||||
self.create_netbox_disk(disk)
|
||||
|
||||
def create_netbox_memory(self, memory):
|
||||
|
|
@ -407,7 +451,7 @@ class Inventory():
|
|||
device=self.device_id,
|
||||
discovered=True,
|
||||
manufacturer=manufacturer.id,
|
||||
tags=[INVENTORY_TAG['memory']['name']],
|
||||
tags=[{'name': INVENTORY_TAG['memory']['name']}],
|
||||
name=name,
|
||||
part_id=memory['product'],
|
||||
serial=memory['serial'],
|
||||
|
|
@ -440,44 +484,66 @@ class Inventory():
|
|||
if memory.get('serial') not in [x.serial for x in nb_memories]:
|
||||
self.create_netbox_memory(memory)
|
||||
|
||||
def create_netbox_gpus(self):
|
||||
for gpu in self.lshw.get_hw_linux('gpu'):
|
||||
def create_netbox_gpus(self, gpus):
|
||||
for gpu in gpus:
|
||||
if 'product' in gpu and len(gpu['product']) > 50:
|
||||
gpu['product'] = (gpu['product'][:48] + '..')
|
||||
|
||||
manufacturer = self.find_or_create_manufacturer(gpu["vendor"])
|
||||
_ = nb.dcim.inventory_items.create(
|
||||
device=self.device_id,
|
||||
manufacturer=manufacturer.id,
|
||||
discovered=True,
|
||||
tags=[INVENTORY_TAG['gpu']['name']],
|
||||
tags=[{'name': INVENTORY_TAG['gpu']['name']}],
|
||||
name=gpu['product'],
|
||||
description='GPU {}'.format(gpu['product']),
|
||||
description=gpu['description'],
|
||||
)
|
||||
|
||||
logging.info('Creating GPU model {}'.format(gpu['product']))
|
||||
|
||||
def is_external_gpu(self, gpu):
|
||||
is_3d_gpu = gpu['description'].startswith('3D')
|
||||
return self.server.is_blade() and \
|
||||
self.server.own_gpu_expansion_slot() and is_3d_gpu
|
||||
|
||||
def do_netbox_gpus(self):
|
||||
gpus = self.lshw.get_hw_linux('gpu')
|
||||
gpus = []
|
||||
gpu_models = {}
|
||||
for gpu in self.lshw.get_hw_linux('gpu'):
|
||||
# Filters GPU if an expansion bay is detected:
|
||||
# The internal (VGA) GPU only goes into the blade inventory,
|
||||
# the external (3D) GPU goes into the expansion blade.
|
||||
if config.expansion_as_device and \
|
||||
self.update_expansion ^ self.is_external_gpu(gpu):
|
||||
continue
|
||||
gpus.append(gpu)
|
||||
gpu_models.setdefault(gpu["product"], 0)
|
||||
gpu_models[gpu["product"]] += 1
|
||||
|
||||
nb_gpus = self.get_netbox_inventory(
|
||||
device_id=self.device_id,
|
||||
tag=INVENTORY_TAG['gpu']['slug'],
|
||||
)
|
||||
|
||||
if not len(nb_gpus) or \
|
||||
len(nb_gpus) and len(gpus) != len(nb_gpus):
|
||||
nb_gpu_models = {}
|
||||
for gpu in nb_gpus:
|
||||
nb_gpu_models.setdefault(str(gpu), 0)
|
||||
nb_gpu_models[str(gpu)] += 1
|
||||
up_to_date = set(gpu_models) == set(nb_gpu_models)
|
||||
if not gpus or not up_to_date:
|
||||
for x in nb_gpus:
|
||||
x.delete()
|
||||
|
||||
self.create_netbox_gpus()
|
||||
if gpus and not up_to_date:
|
||||
self.create_netbox_gpus(gpus)
|
||||
|
||||
def create_or_update(self):
|
||||
if config.inventory is None or config.update_inventory is None:
|
||||
return False
|
||||
self.do_netbox_cpus()
|
||||
self.do_netbox_memories()
|
||||
self.do_netbox_raid_cards()
|
||||
self.do_netbox_disks()
|
||||
self.do_netbox_interfaces()
|
||||
self.do_netbox_motherboard()
|
||||
if self.update_expansion is False:
|
||||
self.do_netbox_cpus()
|
||||
self.do_netbox_memories()
|
||||
self.do_netbox_interfaces()
|
||||
self.do_netbox_motherboard()
|
||||
self.do_netbox_gpus()
|
||||
self.do_netbox_disks()
|
||||
self.do_netbox_raid_cards()
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from netbox_agent.misc import is_tool
|
||||
import subprocess
|
||||
import logging
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
class LSHW():
|
||||
|
|
@ -16,8 +15,13 @@ class LSHW():
|
|||
'lshw -quiet -json'
|
||||
)
|
||||
self.hw_info = json.loads(data)
|
||||
if isinstance(self.hw_info, list):
|
||||
self.hw_info = self.hw_info[0]
|
||||
json_data = json.loads(data)
|
||||
# Starting from version 02.18, `lshw -json` wraps its result in a list
|
||||
# rather than returning directly a dictionary
|
||||
if isinstance(json_data, list):
|
||||
self.hw_info = json_data[0]
|
||||
else:
|
||||
self.hw_info = json_data
|
||||
self.info = {}
|
||||
self.memories = []
|
||||
self.interfaces = []
|
||||
|
|
@ -79,42 +83,41 @@ class LSHW():
|
|||
def find_storage(self, obj):
|
||||
if "children" in obj:
|
||||
for device in obj["children"]:
|
||||
d = {}
|
||||
d["logicalname"] = device.get("logicalname")
|
||||
d["product"] = device.get("product")
|
||||
d["serial"] = device.get("serial")
|
||||
d["version"] = device.get("version")
|
||||
d["size"] = device.get("size")
|
||||
d["description"] = device.get("description")
|
||||
|
||||
self.disks.append(d)
|
||||
|
||||
self.disks.append({
|
||||
"logicalname": device.get("logicalname"),
|
||||
"product": device.get("product"),
|
||||
"serial": device.get("serial"),
|
||||
"version": device.get("version"),
|
||||
"size": device.get("size"),
|
||||
"description": device.get("description"),
|
||||
"type": device.get("description"),
|
||||
})
|
||||
elif "nvme" in obj["configuration"]["driver"]:
|
||||
if not is_tool('nvme'):
|
||||
logging.error('nvme-cli >= 1.0 does not seem to be installed')
|
||||
else:
|
||||
try:
|
||||
nvme = json.loads(
|
||||
subprocess.check_output(
|
||||
["nvme", '-list', '-o', 'json'],
|
||||
encoding='utf8')
|
||||
)
|
||||
|
||||
for device in nvme["Devices"]:
|
||||
d = {}
|
||||
d['logicalname'] = device["DevicePath"]
|
||||
d['product'] = device["ModelNumber"]
|
||||
d['serial'] = device["SerialNumber"]
|
||||
d["version"] = device["Firmware"]
|
||||
if "UsedSize" in device:
|
||||
d['size'] = device["UsedSize"]
|
||||
if "UsedBytes" in device:
|
||||
d['size'] = device["UsedBytes"]
|
||||
d['description'] = "NVME Disk"
|
||||
|
||||
self.disks.append(d)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
try:
|
||||
nvme = json.loads(
|
||||
subprocess.check_output(
|
||||
["nvme", '-list', '-o', 'json'],
|
||||
encoding='utf8')
|
||||
)
|
||||
for device in nvme["Devices"]:
|
||||
d = {
|
||||
'logicalname': device["DevicePath"],
|
||||
'product': device["ModelNumber"],
|
||||
'serial': device["SerialNumber"],
|
||||
"version": device["Firmware"],
|
||||
'description': "NVME",
|
||||
'type': "NVME",
|
||||
}
|
||||
if "UsedSize" in device:
|
||||
d['size'] = device["UsedSize"]
|
||||
if "UsedBytes" in device:
|
||||
d['size'] = device["UsedBytes"]
|
||||
self.disks.append(d)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def find_cpus(self, obj):
|
||||
if "product" in obj:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import socket
|
||||
import subprocess
|
||||
from shutil import which
|
||||
|
||||
from netbox_agent.config import netbox_instance as nb
|
||||
from slugify import slugify
|
||||
from shutil import which
|
||||
import subprocess
|
||||
import socket
|
||||
import re
|
||||
|
||||
|
||||
def is_tool(name):
|
||||
|
|
@ -60,6 +61,7 @@ def get_hostname(config):
|
|||
|
||||
|
||||
def create_netbox_tags(tags):
|
||||
ret = []
|
||||
for tag in tags:
|
||||
nb_tag = nb.extras.tags.get(
|
||||
name=tag
|
||||
|
|
@ -67,5 +69,23 @@ def create_netbox_tags(tags):
|
|||
if not nb_tag:
|
||||
nb_tag = nb.extras.tags.create(
|
||||
name=tag,
|
||||
slug=tag,
|
||||
slug=slugify(tag),
|
||||
)
|
||||
ret.append(nb_tag)
|
||||
return ret
|
||||
|
||||
|
||||
def get_mount_points():
|
||||
mount_points = {}
|
||||
output = subprocess.getoutput('mount')
|
||||
for r in output.split("\n"):
|
||||
if not r.startswith("/dev/"):
|
||||
continue
|
||||
mount_info = r.split()
|
||||
device = mount_info[0]
|
||||
device = re.sub(r'\d+$', '', device)
|
||||
mp = mount_info[2]
|
||||
mount_points.setdefault(device, []).append(mp)
|
||||
return mount_points
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,13 @@ class Network(object):
|
|||
|
||||
ip_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET, [])
|
||||
ip6_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET6, [])
|
||||
if config.network.ignore_ips:
|
||||
for i, ip in enumerate(ip_addr):
|
||||
if re.match(config.network.ignore_ips, ip['addr']):
|
||||
ip_addr.pop(i)
|
||||
for i, ip in enumerate(ip6_addr):
|
||||
if re.match(config.network.ignore_ips, ip['addr']):
|
||||
ip6_addr.pop(i)
|
||||
|
||||
# netifaces returns a ipv6 netmask that netaddr does not understand.
|
||||
# this strips the netmask down to the correct format for netaddr,
|
||||
|
|
@ -77,15 +84,11 @@ class Network(object):
|
|||
addr["netmask"] = addr["netmask"].split('/')[0]
|
||||
ip_addr.append(addr)
|
||||
|
||||
if config.network.ignore_ips and ip_addr:
|
||||
for i, ip in enumerate(ip_addr):
|
||||
if re.match(config.network.ignore_ips, ip['addr']):
|
||||
ip_addr.pop(i)
|
||||
|
||||
mac = open('/sys/class/net/{}/address'.format(interface), 'r').read().strip()
|
||||
vlan = None
|
||||
if len(interface.split('.')) > 1:
|
||||
vlan = int(interface.split('.')[1])
|
||||
|
||||
bonding = False
|
||||
bonding_slaves = []
|
||||
if os.path.isdir('/sys/class/net/{}/bonding'.format(interface)):
|
||||
|
|
@ -145,19 +148,19 @@ class Network(object):
|
|||
if nic['mac'] is None:
|
||||
interface = self.nb_net.interfaces.get(
|
||||
name=nic['name'],
|
||||
**self.custom_arg_id,
|
||||
**self.custom_arg_id
|
||||
)
|
||||
else:
|
||||
interface = self.nb_net.interfaces.get(
|
||||
mac_address=nic['mac'],
|
||||
name=nic['name'],
|
||||
**self.custom_arg_id,
|
||||
**self.custom_arg_id
|
||||
)
|
||||
return interface
|
||||
|
||||
def get_netbox_network_cards(self):
|
||||
return self.nb_net.interfaces.filter(
|
||||
**self.custom_arg_id,
|
||||
**self.custom_arg_id
|
||||
)
|
||||
|
||||
def get_netbox_type_for_nic(self, nic):
|
||||
|
|
@ -209,8 +212,12 @@ class Network(object):
|
|||
update = False
|
||||
vlan_id = nic['vlan']
|
||||
lldp_vlan = self.lldp.get_switch_vlan(nic['name']) if config.network.lldp else None
|
||||
# For strange reason, we need to get the object from scratch
|
||||
# The object returned by pynetbox's save isn't always working (since pynetbox 6)
|
||||
interface = nb.dcim.interfaces.get(id=interface.id)
|
||||
|
||||
# if local interface isn't a interface vlan or lldp doesn't report a vlan-id
|
||||
# Handle the case were the local interface isn't an interface vlan as reported by Netbox
|
||||
# and that LLDP doesn't report a vlan-id
|
||||
if vlan_id is None and lldp_vlan is None and \
|
||||
(interface.mode is not None or len(interface.tagged_vlans) > 0):
|
||||
logging.info('Interface {interface} is not tagged, reseting mode'.format(
|
||||
|
|
@ -219,13 +226,16 @@ class Network(object):
|
|||
interface.mode = None
|
||||
interface.tagged_vlans = []
|
||||
interface.untagged_vlan = None
|
||||
# if it's a vlan interface
|
||||
# if the local interface is configured with a vlan, it's supposed to be taggued
|
||||
# if mode is either not set or not correctly configured or vlan are not
|
||||
# correctly configured, we reset the vlan
|
||||
elif vlan_id and (
|
||||
interface.mode is None or
|
||||
type(interface.mode) is not int and (
|
||||
hasattr(interface.mode, 'value') and
|
||||
interface.mode.value == self.dcim_choices['interface:mode']['Access'] or
|
||||
len(interface.tagged_vlans) != 1 or
|
||||
interface.tagged_vlans[0].vid != vlan_id)):
|
||||
int(interface.tagged_vlans[0].vid) != int(vlan_id))):
|
||||
logging.info('Resetting tagged VLAN(s) on interface {interface}'.format(
|
||||
interface=interface))
|
||||
update = True
|
||||
|
|
@ -233,7 +243,7 @@ class Network(object):
|
|||
interface.mode = self.dcim_choices['interface:mode']['Tagged']
|
||||
interface.tagged_vlans = [nb_vlan] if nb_vlan else []
|
||||
interface.untagged_vlan = None
|
||||
# if lldp reports a vlan-id with pvid
|
||||
# Finally if LLDP reports a vlan-id with the pvid attribute
|
||||
elif lldp_vlan:
|
||||
pvid_vlan = [key for (key, value) in lldp_vlan.items() if value['pvid']]
|
||||
if len(pvid_vlan) > 0 and (
|
||||
|
|
@ -257,12 +267,12 @@ class Network(object):
|
|||
|
||||
nb_vlan = None
|
||||
|
||||
params = {
|
||||
params = dict(self.custom_arg)
|
||||
params.update({
|
||||
'name': nic['name'],
|
||||
'type': type,
|
||||
'mgmt_only': mgmt,
|
||||
**self.custom_arg,
|
||||
}
|
||||
})
|
||||
|
||||
if not nic.get('virtual', False):
|
||||
params['mac_address'] = nic['mac']
|
||||
|
|
@ -271,7 +281,7 @@ class Network(object):
|
|||
|
||||
if nic['vlan']:
|
||||
nb_vlan = self.get_or_create_vlan(nic['vlan'])
|
||||
interface.mode = 200
|
||||
interface.mode = self.dcim_choices['interface:mode']['Tagged']
|
||||
interface.tagged_vlans = [nb_vlan.id]
|
||||
interface.save()
|
||||
elif config.network.lldp and self.lldp.get_switch_vlan(nic['name']) is not None:
|
||||
|
|
@ -315,57 +325,71 @@ class Network(object):
|
|||
netbox_ips = nb.ipam.ip_addresses.filter(
|
||||
address=ip,
|
||||
)
|
||||
if not len(netbox_ips):
|
||||
if not netbox_ips:
|
||||
logging.info('Create new IP {ip} on {interface}'.format(
|
||||
ip=ip, interface=interface))
|
||||
query_params = {
|
||||
'address': ip,
|
||||
'status': "active",
|
||||
'assigned_object_type': self.assigned_object_type,
|
||||
'assigned_object_id': interface.id
|
||||
}
|
||||
|
||||
netbox_ip = nb.ipam.ip_addresses.create(
|
||||
address=ip,
|
||||
interface=interface.id,
|
||||
status=1,
|
||||
**query_params
|
||||
)
|
||||
else:
|
||||
netbox_ip = netbox_ips[0]
|
||||
# If IP exists in anycast
|
||||
if netbox_ip.role and netbox_ip.role.label == 'Anycast':
|
||||
logging.debug('IP {} is Anycast..'.format(ip))
|
||||
unassigned_anycast_ip = [x for x in netbox_ips if x.interface is None]
|
||||
assigned_anycast_ip = [x for x in netbox_ips if
|
||||
x.interface and x.interface.id == interface.id]
|
||||
# use the first available anycast ip
|
||||
if len(unassigned_anycast_ip):
|
||||
logging.info('Assigning existing Anycast IP {} to interface'.format(ip))
|
||||
netbox_ip = unassigned_anycast_ip[0]
|
||||
netbox_ip.interface = interface
|
||||
netbox_ip.save()
|
||||
# or if everything is assigned to other servers
|
||||
elif not len(assigned_anycast_ip):
|
||||
logging.info('Creating Anycast IP {} and assigning it to interface'.format(ip))
|
||||
netbox_ip = nb.ipam.ip_addresses.create(
|
||||
address=ip,
|
||||
interface=interface.id,
|
||||
status=1,
|
||||
role=self.ipam_choices['ip-address:role']['Anycast'],
|
||||
tenant=self.tenant.id if self.tenant else None,
|
||||
)
|
||||
return netbox_ip
|
||||
else:
|
||||
if netbox_ip.interface is None:
|
||||
logging.info('Assigning existing IP {ip} to {interface}'.format(
|
||||
ip=ip, interface=interface))
|
||||
elif netbox_ip.interface.id != interface.id:
|
||||
logging.info(
|
||||
'Detected interface change for ip {ip}: old interface is '
|
||||
'{old_interface} (id: {old_id}), new interface is {new_interface} '
|
||||
' (id: {new_id})'
|
||||
.format(
|
||||
old_interface=netbox_ip.interface, new_interface=interface,
|
||||
old_id=netbox_ip.id, new_id=interface.id, ip=netbox_ip.address
|
||||
))
|
||||
else:
|
||||
return netbox_ip
|
||||
return netbox_ip
|
||||
|
||||
netbox_ip = list(netbox_ips)[0]
|
||||
# If IP exists in anycast
|
||||
if netbox_ip.role and netbox_ip.role.label == 'Anycast':
|
||||
logging.debug('IP {} is Anycast..'.format(ip))
|
||||
unassigned_anycast_ip = [x for x in netbox_ips if x.interface is None]
|
||||
assigned_anycast_ip = [x for x in netbox_ips if
|
||||
x.interface and x.interface.id == interface.id]
|
||||
# use the first available anycast ip
|
||||
if len(unassigned_anycast_ip):
|
||||
logging.info('Assigning existing Anycast IP {} to interface'.format(ip))
|
||||
netbox_ip = unassigned_anycast_ip[0]
|
||||
netbox_ip.interface = interface
|
||||
netbox_ip.save()
|
||||
return netbox_ip
|
||||
# or if everything is assigned to other servers
|
||||
elif not len(assigned_anycast_ip):
|
||||
logging.info('Creating Anycast IP {} and assigning it to interface'.format(ip))
|
||||
query_params = {
|
||||
"address": ip,
|
||||
"status": "active",
|
||||
"role": self.ipam_choices['ip-address:role']['Anycast'],
|
||||
"tenant": self.tenant.id if self.tenant else None,
|
||||
"assigned_object_type": self.assigned_object_type,
|
||||
"assigned_object_id": interface.id
|
||||
}
|
||||
netbox_ip = nb.ipam.ip_addresses.create(**query_params)
|
||||
return netbox_ip
|
||||
else:
|
||||
ip_interface = getattr(netbox_ip, 'interface', None)
|
||||
assigned_object = getattr(netbox_ip, 'assigned_object', None)
|
||||
if not ip_interface or not assigned_object:
|
||||
logging.info('Assigning existing IP {ip} to {interface}'.format(
|
||||
ip=ip, interface=interface))
|
||||
elif (ip_interface and ip_interface.id != interface.id) or \
|
||||
(assigned_object and assigned_object_id != interface.id):
|
||||
|
||||
old_interface = getattr(netbox_ip, "assigned_object", "n/a")
|
||||
logging.info(
|
||||
'Detected interface change for ip {ip}: old interface is '
|
||||
'{old_interface} (id: {old_id}), new interface is {new_interface} '
|
||||
' (id: {new_id})'
|
||||
.format(
|
||||
old_interface=old_interface, new_interface=interface,
|
||||
old_id=netbox_ip.id, new_id=interface.id, ip=netbox_ip.address
|
||||
))
|
||||
else:
|
||||
return netbox_ip
|
||||
|
||||
netbox_ip.assigned_object_type = self.assigned_object_type
|
||||
netbox_ip.assigned_object_id = interface.id
|
||||
netbox_ip.save()
|
||||
|
||||
def create_or_update_netbox_network_cards(self):
|
||||
if config.update_all is None or config.update_network is None:
|
||||
|
|
@ -373,9 +397,9 @@ class Network(object):
|
|||
logging.debug('Creating/Updating NIC...')
|
||||
|
||||
# delete unknown interface
|
||||
nb_nics = self.get_netbox_network_cards()
|
||||
nb_nics = list(self.get_netbox_network_cards())
|
||||
local_nics = [x['name'] for x in self.nics]
|
||||
for nic in nb_nics[:]:
|
||||
for nic in nb_nics:
|
||||
if nic.name not in local_nics:
|
||||
logging.info('Deleting netbox interface {name} because not present locally'.format(
|
||||
name=nic.name
|
||||
|
|
@ -386,16 +410,19 @@ class Network(object):
|
|||
# delete IP on netbox that are not known on this server
|
||||
if len(nb_nics):
|
||||
netbox_ips = nb.ipam.ip_addresses.filter(
|
||||
interface_id=[x.id for x in nb_nics],
|
||||
**{self.intf_type: [x.id for x in nb_nics]}
|
||||
)
|
||||
|
||||
netbox_ips = list(netbox_ips)
|
||||
all_local_ips = list(chain.from_iterable([
|
||||
x['ip'] for x in self.nics if x['ip'] is not None
|
||||
]))
|
||||
for netbox_ip in netbox_ips:
|
||||
if netbox_ip.address not in all_local_ips:
|
||||
logging.info('Unassigning IP {ip} from {interface}'.format(
|
||||
ip=netbox_ip.address, interface=netbox_ip.interface))
|
||||
netbox_ip.interface = None
|
||||
ip=netbox_ip.address, interface=netbox_ip.assigned_object))
|
||||
netbox_ip.assigned_object_type = None
|
||||
netbox_ip.assigned_object_id = None
|
||||
netbox_ip.save()
|
||||
|
||||
# update each nic
|
||||
|
|
@ -417,12 +444,13 @@ class Network(object):
|
|||
ret, interface = self.reset_vlan_on_interface(nic, interface)
|
||||
nic_update += ret
|
||||
|
||||
_type = self.get_netbox_type_for_nic(nic)
|
||||
if not interface.type or \
|
||||
_type != interface.type.value:
|
||||
logging.info('Interface type is wrong, resetting')
|
||||
interface.type = _type
|
||||
nic_update += 1
|
||||
if hasattr(interface, 'type'):
|
||||
_type = self.get_netbox_type_for_nic(nic)
|
||||
if not interface.type or \
|
||||
_type != interface.type.value:
|
||||
logging.info('Interface type is wrong, resetting')
|
||||
interface.type = _type
|
||||
nic_update += 1
|
||||
|
||||
if hasattr(interface, 'lag') and interface.lag is not None:
|
||||
local_lag_int = next(
|
||||
|
|
@ -465,6 +493,8 @@ class ServerNetwork(Network):
|
|||
self.nb_net = nb.dcim
|
||||
self.custom_arg = {'device': getattr(self.device, "id", None)}
|
||||
self.custom_arg_id = {'device_id': getattr(self.device, "id", None)}
|
||||
self.intf_type = "interface_id"
|
||||
self.assigned_object_type = "dcim.interface"
|
||||
|
||||
def get_network_type(self):
|
||||
return 'server'
|
||||
|
|
@ -485,7 +515,7 @@ class ServerNetwork(Network):
|
|||
return nb_server_interface
|
||||
|
||||
try:
|
||||
nb_switch = nb_mgmt_ip.interface.device
|
||||
nb_switch = nb_mgmt_ip.assigned_object.device
|
||||
logging.info('Found a switch in Netbox based on LLDP infos: {} (id: {})'.format(
|
||||
switch_ip,
|
||||
nb_switch.id
|
||||
|
|
@ -585,6 +615,8 @@ class VirtualNetwork(Network):
|
|||
self.nb_net = nb.virtualization
|
||||
self.custom_arg = {'virtual_machine': getattr(self.device, "id", None)}
|
||||
self.custom_arg_id = {'virtual_machine_id': getattr(self.device, "id", None)}
|
||||
self.intf_type = "vminterface_id"
|
||||
self.assigned_object_type = "virtualization.vminterface"
|
||||
|
||||
dcim_c = nb.virtualization.interfaces.choices()
|
||||
for _choice_type in dcim_c:
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class PowerSupply():
|
|||
)
|
||||
|
||||
def create_or_update_power_supply(self):
|
||||
nb_psus = self.get_netbox_power_supply()
|
||||
nb_psus = list(self.get_netbox_power_supply())
|
||||
psus = self.get_power_supply()
|
||||
|
||||
# Delete unknown PSU
|
||||
|
|
@ -105,18 +105,20 @@ class PowerSupply():
|
|||
return False
|
||||
|
||||
# find power feeds for rack or dc
|
||||
voltage = None
|
||||
pwr_feeds = None
|
||||
if self.netbox_server.rack:
|
||||
pwr_feeds = nb.dcim.power_feeds.filter(
|
||||
rack=self.netbox_server.rack.id
|
||||
)
|
||||
if pwr_feeds is None or not len(pwr_feeds):
|
||||
|
||||
if pwr_feeds:
|
||||
voltage = [p['voltage'] for p in pwr_feeds]
|
||||
else:
|
||||
logging.info('Could not find power feeds for Rack, defaulting value to 230')
|
||||
voltage = 230
|
||||
voltage = [230 for _ in nb_psus]
|
||||
|
||||
for i, nb_psu in enumerate(nb_psus):
|
||||
nb_psu.allocated_draw = float(psu_cons[i]) * voltage
|
||||
nb_psu.allocated_draw = int(float(psu_cons[i]) * voltage[i])
|
||||
if nb_psu.allocated_draw < 1:
|
||||
logging.info('PSU is not connected or in standby mode')
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ class RaidController():
|
|||
def get_physical_disks(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def is_external(self):
|
||||
return False
|
||||
|
||||
|
||||
class Raid():
|
||||
def get_controllers(self):
|
||||
|
|
|
|||
|
|
@ -1,106 +1,99 @@
|
|||
import re
|
||||
import subprocess
|
||||
|
||||
from netbox_agent.misc import get_vendor
|
||||
from netbox_agent.raid.base import Raid, RaidController
|
||||
from netbox_agent.misc import get_vendor
|
||||
from netbox_agent.config import config
|
||||
import subprocess
|
||||
import logging
|
||||
import re
|
||||
|
||||
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 ssacli(command):
|
||||
output = subprocess.getoutput('ssacli {}'.format(command) )
|
||||
lines = output.split('\n')
|
||||
lines = list(filter(None, lines))
|
||||
return lines
|
||||
|
||||
|
||||
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
|
||||
def _parse_ctrl_output(lines):
|
||||
controllers = {}
|
||||
current_ctrl = 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
|
||||
for line in lines:
|
||||
if not line or line.startswith('Note:'):
|
||||
continue
|
||||
|
||||
current_line_indentation = _get_indentation(current_line)
|
||||
# This check ignore some useless information that make
|
||||
# crash the parsing
|
||||
product_name = REGEXP_CONTROLLER_HP.search(current_line)
|
||||
if current_line_indentation == 0 and not product_name:
|
||||
i = i + 1
|
||||
ctrl = REGEXP_CONTROLLER_HP.search(line)
|
||||
if ctrl is not None:
|
||||
current_ctrl = ctrl.group(1)
|
||||
controllers[current_ctrl] = {'Slot': ctrl.group(2)}
|
||||
if 'Embedded' not in line:
|
||||
controllers[current_ctrl]['External'] = True
|
||||
continue
|
||||
attr, val = line.split(': ', 1)
|
||||
attr = attr.strip()
|
||||
val = val.strip()
|
||||
controllers[current_ctrl][attr] = val
|
||||
return controllers
|
||||
|
||||
if current_line_indentation == indentation:
|
||||
current_item = current_line.lstrip(' ')
|
||||
|
||||
info[current_item] = {}
|
||||
i = i + 1
|
||||
def _parse_pd_output(lines):
|
||||
drives = {}
|
||||
current_array = None
|
||||
current_drv = None
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('Note:'):
|
||||
continue
|
||||
# Parses the Array the drives are in
|
||||
if line.startswith('Array'):
|
||||
current_array = line.split(None, 1)[1]
|
||||
# Detects new physical drive
|
||||
if line.startswith('physicaldrive'):
|
||||
current_drv = line.split(None, 1)[1]
|
||||
drives[current_drv] = {}
|
||||
if current_array is not None:
|
||||
drives[current_drv]['Array'] = current_array
|
||||
continue
|
||||
if ': ' not in line:
|
||||
continue
|
||||
attr, val = line.split(': ', 1)
|
||||
drives.setdefault(current_drv, {})[attr] = val
|
||||
return drives
|
||||
|
||||
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)
|
||||
def _parse_ld_output(lines):
|
||||
drives = {}
|
||||
current_array = None
|
||||
current_drv = None
|
||||
|
||||
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
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('Note:'):
|
||||
continue
|
||||
# Parses the Array the drives are in
|
||||
if line.startswith('Array'):
|
||||
current_array = line.split(None, 1)[1]
|
||||
drives[current_array] = {}
|
||||
# Detects new physical drive
|
||||
if line.startswith('Logical Drive'):
|
||||
current_drv = line.split(': ', 1)[1]
|
||||
drives.setdefault(current_array, {})['LogicalDrive'] = current_drv
|
||||
continue
|
||||
if ': ' not in line:
|
||||
continue
|
||||
attr, val = line.split(': ', 1)
|
||||
drives.setdefault(current_array, {})[attr] = val
|
||||
return drives
|
||||
|
||||
|
||||
class HPRaidController(RaidController):
|
||||
def __init__(self, controller_name, data):
|
||||
self.controller_name = controller_name
|
||||
self.data = data
|
||||
self.pdrives = self._get_physical_disks()
|
||||
self.ldrives = self._get_logical_drives()
|
||||
self._get_virtual_drives_map()
|
||||
|
||||
def get_product_name(self):
|
||||
return self.controller_name
|
||||
|
|
@ -114,40 +107,69 @@ class HPRaidController(RaidController):
|
|||
def get_firmware_version(self):
|
||||
return self.data['Firmware Version']
|
||||
|
||||
def get_physical_disks(self):
|
||||
ret = []
|
||||
output = subprocess.getoutput(
|
||||
'ssacli ctrl slot={slot} pd all show detail'.format(slot=self.data['Slot'])
|
||||
)
|
||||
lines = output.split('\n')
|
||||
lines = list(filter(None, lines))
|
||||
j = -1
|
||||
while j < len(lines):
|
||||
info_dict, j = _get_dict(lines, j + 1, 0)
|
||||
def is_external(self):
|
||||
return self.data.get('External', False)
|
||||
|
||||
key = next(iter(info_dict))
|
||||
for array, physical_disk in info_dict[key].items():
|
||||
for _, pd_attr in physical_disk.items():
|
||||
model = pd_attr.get('Model', '').strip()
|
||||
vendor = None
|
||||
if model.startswith('HP'):
|
||||
vendor = 'HP'
|
||||
elif len(model.split()) > 1:
|
||||
vendor = get_vendor(model.split()[1])
|
||||
else:
|
||||
vendor = get_vendor(model)
|
||||
def _get_physical_disks(self):
|
||||
lines = ssacli('ctrl slot={} pd all show detail'.format(self.data['Slot']))
|
||||
pdrives = _parse_pd_output(lines)
|
||||
ret = {}
|
||||
|
||||
ret.append({
|
||||
'Model': model,
|
||||
'Vendor': vendor,
|
||||
'SN': pd_attr.get('Serial Number', '').strip(),
|
||||
'Size': pd_attr.get('Size', '').strip(),
|
||||
'Type': 'SSD' if pd_attr.get('Interface Type') == 'Solid State SATA'
|
||||
else 'HDD',
|
||||
'_src': self.__class__.__name__,
|
||||
})
|
||||
for name, attrs in pdrives.items():
|
||||
array = attrs.get('Array', '')
|
||||
model = attrs.get('Model', '').strip()
|
||||
vendor = None
|
||||
if model.startswith('HP'):
|
||||
vendor = 'HP'
|
||||
elif len(model.split()) > 1:
|
||||
vendor = get_vendor(model.split()[1])
|
||||
else:
|
||||
vendor = get_vendor(model)
|
||||
|
||||
ret[name] = {
|
||||
'Array': array,
|
||||
'Model': model,
|
||||
'Vendor': vendor,
|
||||
'SN': attrs.get('Serial Number', '').strip(),
|
||||
'Size': attrs.get('Size', '').strip(),
|
||||
'Type': 'SSD' if attrs.get('Interface Type') == 'Solid State SATA'
|
||||
else 'HDD',
|
||||
'_src': self.__class__.__name__,
|
||||
}
|
||||
return ret
|
||||
|
||||
def _get_logical_drives(self):
|
||||
lines = ssacli('ctrl slot={} ld all show detail'.format(self.data['Slot']))
|
||||
ldrives = _parse_ld_output(lines)
|
||||
ret = {}
|
||||
|
||||
for array, attrs in ldrives.items():
|
||||
ret[array] = {
|
||||
'vd_array': array,
|
||||
'vd_size': attrs['Size'],
|
||||
'vd_consistency': attrs['Status'],
|
||||
'vd_raid_type': 'RAID {}'.format(attrs['Fault Tolerance']),
|
||||
'vd_device': attrs['LogicalDrive'],
|
||||
'mount_point': attrs['Mount Points']
|
||||
}
|
||||
return ret
|
||||
|
||||
def _get_virtual_drives_map(self):
|
||||
for name, attrs in self.pdrives.items():
|
||||
array = attrs["Array"]
|
||||
ld = self.ldrives.get(array)
|
||||
if ld is None:
|
||||
logging.error(
|
||||
"Failed to find array information for physical drive {}."
|
||||
" Ignoring.".format(name)
|
||||
)
|
||||
continue
|
||||
attrs['custom_fields'] = ld
|
||||
attrs['custom_fields']['pd_identifier'] = name
|
||||
|
||||
def get_physical_disks(self):
|
||||
return list(self.pdrives.values())
|
||||
|
||||
|
||||
class HPRaid(Raid):
|
||||
def __init__(self):
|
||||
|
|
@ -158,16 +180,11 @@ class HPRaid(Raid):
|
|||
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])
|
||||
)
|
||||
controllers = _parse_ctrl_output(lines)
|
||||
for controller, attrs in controllers.items():
|
||||
self.controllers.append(
|
||||
HPRaidController(controller, attrs)
|
||||
)
|
||||
|
||||
def get_controllers(self):
|
||||
return self.controllers
|
||||
|
|
|
|||
|
|
@ -1,25 +1,32 @@
|
|||
import re
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET # NOQA
|
||||
|
||||
from netbox_agent.misc import get_vendor
|
||||
from netbox_agent.raid.base import Raid, RaidController
|
||||
|
||||
# Inspiration from https://github.com/asciiphil/perc-status/blob/master/perc-status
|
||||
from netbox_agent.misc import get_vendor, get_mount_points
|
||||
from netbox_agent.config import config
|
||||
import subprocess
|
||||
import logging
|
||||
import re
|
||||
|
||||
|
||||
def get_field(obj, fieldname):
|
||||
f = obj.find(fieldname)
|
||||
if f is None:
|
||||
return None
|
||||
if f.attrib['type'] in ['u32', 'u64']:
|
||||
if re.search('Mask$', fieldname):
|
||||
return int(f.text, 2)
|
||||
else:
|
||||
return int(f.text)
|
||||
if f.attrib['type'] == 'astring':
|
||||
return f.text
|
||||
return f.text
|
||||
def omreport(sub_command):
|
||||
command = 'omreport {}'.format(sub_command)
|
||||
output = subprocess.getoutput(command)
|
||||
res = {}
|
||||
section_re = re.compile('^[A-Z]')
|
||||
current_section = None
|
||||
current_obj = None
|
||||
|
||||
for line in output.split('\n'):
|
||||
if ': ' in line:
|
||||
attr, value = line.split(': ', 1)
|
||||
attr = attr.strip()
|
||||
value = value.strip()
|
||||
if attr == 'ID':
|
||||
obj = {}
|
||||
res.setdefault(current_section, []).append(obj)
|
||||
current_obj = obj
|
||||
current_obj[attr] = value
|
||||
elif section_re.search(line) is not None:
|
||||
current_section = line.strip()
|
||||
return res
|
||||
|
||||
|
||||
class OmreportController(RaidController):
|
||||
|
|
@ -28,49 +35,88 @@ class OmreportController(RaidController):
|
|||
self.controller_index = controller_index
|
||||
|
||||
def get_product_name(self):
|
||||
return get_field(self.data, 'Name')
|
||||
return self.data['Name']
|
||||
|
||||
def get_manufacturer(self):
|
||||
return None
|
||||
|
||||
def get_serial_number(self):
|
||||
return get_field(self.data, 'DeviceSerialNumber')
|
||||
return self.data.get('DeviceSerialNumber')
|
||||
|
||||
def get_firmware_version(self):
|
||||
return get_field(self.data, 'Firmware Version')
|
||||
return self.data.get('Firmware Version')
|
||||
|
||||
def _get_physical_disks(self):
|
||||
pds = {}
|
||||
res = omreport('storage pdisk controller={}'.format(
|
||||
self.controller_index
|
||||
))
|
||||
for pdisk in [d for d in list(res.values())[0]]:
|
||||
disk_id = pdisk['ID']
|
||||
size = re.sub('B .*$', 'B', pdisk['Capacity'])
|
||||
pds[disk_id] = {
|
||||
'Vendor': get_vendor(pdisk['Vendor ID']),
|
||||
'Model': pdisk['Product ID'],
|
||||
'SN': pdisk['Serial No.'],
|
||||
'Size': size,
|
||||
'Type': pdisk['Media'],
|
||||
'_src': self.__class__.__name__,
|
||||
}
|
||||
return pds
|
||||
|
||||
def _get_virtual_drives_map(self):
|
||||
pds = {}
|
||||
res = omreport('storage vdisk controller={}'.format(
|
||||
self.controller_index
|
||||
))
|
||||
for vdisk in [d for d in list(res.values())[0]]:
|
||||
vdisk_id = vdisk['ID']
|
||||
device = vdisk['Device Name']
|
||||
mount_points = get_mount_points()
|
||||
mp = mount_points.get(device, 'n/a')
|
||||
size = re.sub('B .*$', 'B', vdisk['Size'])
|
||||
vd = {
|
||||
'vd_array': vdisk_id,
|
||||
'vd_size': size,
|
||||
'vd_consistency': vdisk['State'],
|
||||
'vd_raid_type': vdisk['Layout'],
|
||||
'vd_device': vdisk['Device Name'],
|
||||
'mount_point': ', '.join(sorted(mp)),
|
||||
}
|
||||
drives_res = omreport(
|
||||
'storage pdisk controller={} vdisk={}'.format(
|
||||
self.controller_index, vdisk_id
|
||||
))
|
||||
for pdisk in [d for d in list(drives_res.values())[0]]:
|
||||
pds[pdisk['ID']] = vd
|
||||
return pds
|
||||
|
||||
def get_physical_disks(self):
|
||||
ret = []
|
||||
output = subprocess.getoutput(
|
||||
'omreport storage controller controller={} -fmt xml'.format(self.controller_index)
|
||||
)
|
||||
root = ET.fromstring(output)
|
||||
et_array_disks = root.find('ArrayDisks')
|
||||
if et_array_disks is not None:
|
||||
for obj in et_array_disks.findall('DCStorageObject'):
|
||||
ret.append({
|
||||
'Vendor': get_vendor(get_field(obj, 'Vendor')),
|
||||
'Model': get_field(obj, 'ProductID'),
|
||||
'SN': get_field(obj, 'DeviceSerialNumber'),
|
||||
'Size': '{:.0f}GB'.format(
|
||||
int(get_field(obj, 'Length')) / 1024 / 1024 / 1024
|
||||
),
|
||||
'Type': 'HDD' if int(get_field(obj, 'MediaType')) == 1 else 'SSD',
|
||||
'_src': self.__class__.__name__,
|
||||
})
|
||||
return ret
|
||||
pds = self._get_physical_disks()
|
||||
vds = self._get_virtual_drives_map()
|
||||
for pd_identifier, vd in vds.items():
|
||||
if pd_identifier not in pds:
|
||||
logging.error(
|
||||
'Physical drive {} listed in virtual drive {} not '
|
||||
'found in drives list'.format(
|
||||
pd_identifier, vd['vd_array']
|
||||
)
|
||||
)
|
||||
continue
|
||||
pds[pd_identifier].setdefault('custom_fields', {}).update(vd)
|
||||
pds[pd_identifier]['custom_fields']['pd_identifier'] = pd_identifier
|
||||
return list(pds.values())
|
||||
|
||||
|
||||
class OmreportRaid(Raid):
|
||||
def __init__(self):
|
||||
output = subprocess.getoutput('omreport storage controller -fmt xml')
|
||||
controller_xml = ET.fromstring(output)
|
||||
self.controllers = []
|
||||
res = omreport('storage controller')
|
||||
|
||||
for obj in controller_xml.find('Controllers').findall('DCStorageObject'):
|
||||
ctrl_index = get_field(obj, 'ControllerNum')
|
||||
for controller in res['Controller']:
|
||||
ctrl_index = controller['ID']
|
||||
self.controllers.append(
|
||||
OmreportController(ctrl_index, obj)
|
||||
OmreportController(ctrl_index, controller)
|
||||
)
|
||||
|
||||
def get_controllers(self):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,31 @@
|
|||
import json
|
||||
import subprocess
|
||||
|
||||
from netbox_agent.misc import get_vendor
|
||||
from netbox_agent.raid.base import Raid, RaidController
|
||||
from netbox_agent.misc import get_vendor, get_mount_points
|
||||
from netbox_agent.config import config
|
||||
import subprocess
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
def storecli(sub_command):
|
||||
command = 'storcli {} J'.format(sub_command)
|
||||
output = subprocess.getoutput(command)
|
||||
data = json.loads(output)
|
||||
controllers = dict([
|
||||
(
|
||||
c['Command Status']['Controller'],
|
||||
c['Response Data']
|
||||
) for c in data['Controllers']
|
||||
if c['Command Status']['Status'] == 'Success'
|
||||
])
|
||||
if not controllers:
|
||||
logging.error(
|
||||
"Failed to execute command '{}'. "
|
||||
"Ignoring data.".format(command)
|
||||
)
|
||||
return {}
|
||||
return controllers
|
||||
|
||||
|
||||
class StorcliController(RaidController):
|
||||
|
|
@ -22,52 +45,101 @@ class StorcliController(RaidController):
|
|||
def get_firmware_version(self):
|
||||
return self.data['FW Package Build']
|
||||
|
||||
def get_physical_disks(self):
|
||||
ret = []
|
||||
output = subprocess.getoutput(
|
||||
'storcli /c{}/eall/sall show all J'.format(self.controller_index)
|
||||
)
|
||||
drive_infos = json.loads(output)['Controllers'][self.controller_index]['Response Data']
|
||||
def _get_physical_disks(self):
|
||||
pds = {}
|
||||
cmd = '/c{}/eall/sall show all'.format(self.controller_index)
|
||||
controllers = storecli(cmd)
|
||||
pd_info = controllers[self.controller_index]
|
||||
pd_re = re.compile(r'^Drive (/c\d+/e\d+/s\d+)$')
|
||||
|
||||
for physical_drive in self.data['PD LIST']:
|
||||
enclosure = physical_drive.get('EID:Slt').split(':')[0]
|
||||
slot = physical_drive.get('EID:Slt').split(':')[1]
|
||||
size = physical_drive.get('Size').strip()
|
||||
media_type = physical_drive.get('Med').strip()
|
||||
drive_identifier = 'Drive /c{}/e{}/s{}'.format(
|
||||
str(self.controller_index), str(enclosure), str(slot)
|
||||
)
|
||||
drive_attr = drive_infos['{} - Detailed Information'.format(drive_identifier)][
|
||||
'{} Device attributes'.format(drive_identifier)]
|
||||
model = drive_attr.get('Model Number', '').strip()
|
||||
ret.append({
|
||||
for section, attrs in pd_info.items():
|
||||
reg = pd_re.search(section)
|
||||
if reg is None:
|
||||
continue
|
||||
pd_name = reg.group(1)
|
||||
pd_attr = attrs[0]
|
||||
pd_identifier = pd_attr['EID:Slt']
|
||||
size = pd_attr.get('Size', '').strip()
|
||||
media_type = pd_attr.get('Med', '').strip()
|
||||
pd_details = pd_info['{} - Detailed Information'.format(section)]
|
||||
pd_dev_attr = pd_details['{} Device attributes'.format(section)]
|
||||
model = pd_dev_attr.get('Model Number', '').strip()
|
||||
pd = {
|
||||
'Model': model,
|
||||
'Vendor': get_vendor(model),
|
||||
'SN': drive_attr.get('SN', '').strip(),
|
||||
'SN': pd_dev_attr.get('SN', '').strip(),
|
||||
'Size': size,
|
||||
'Type': media_type,
|
||||
'_src': self.__class__.__name__,
|
||||
})
|
||||
return ret
|
||||
}
|
||||
if config.process_virtual_drives:
|
||||
pd.setdefault('custom_fields', {})['pd_identifier'] = pd_name
|
||||
pds[pd_identifier] = pd
|
||||
return pds
|
||||
|
||||
def _get_virtual_drives_map(self):
|
||||
vds = {}
|
||||
cmd = '/c{}/vall show all'.format(self.controller_index)
|
||||
controllers = storecli(cmd)
|
||||
vd_info = controllers[self.controller_index]
|
||||
mount_points = get_mount_points()
|
||||
|
||||
for vd_identifier, vd_attrs in vd_info.items():
|
||||
if not vd_identifier.startswith("/c{}/v".format(self.controller_index)):
|
||||
continue
|
||||
volume = vd_identifier.split("/")[-1].lstrip("v")
|
||||
vd_attr = vd_attrs[0]
|
||||
vd_pd_identifier = 'PDs for VD {}'.format(volume)
|
||||
vd_pds = vd_info[vd_pd_identifier]
|
||||
vd_prop_identifier = 'VD{} Properties'.format(volume)
|
||||
vd_properties = vd_info[vd_prop_identifier]
|
||||
for pd in vd_pds:
|
||||
pd_identifier = pd["EID:Slt"]
|
||||
wwn = vd_properties["SCSI NAA Id"]
|
||||
wwn_path = "/dev/disk/by-id/wwn-0x{}".format(wwn)
|
||||
device = os.path.realpath(wwn_path)
|
||||
mp = mount_points.get(device, "n/a")
|
||||
vds[pd_identifier] = {
|
||||
"vd_array": vd_identifier,
|
||||
"vd_size": vd_attr["Size"],
|
||||
"vd_consistency": vd_attr["Consist"],
|
||||
"vd_raid_type": vd_attr["TYPE"],
|
||||
"vd_device": device,
|
||||
"mount_point": ", ".join(sorted(mp))
|
||||
}
|
||||
return vds
|
||||
|
||||
def get_physical_disks(self):
|
||||
# Parses physical disks information
|
||||
pds = self._get_physical_disks()
|
||||
|
||||
# Parses virtual drives information and maps them to physical disks
|
||||
vds = self._get_virtual_drives_map()
|
||||
for pd_identifier, vd in vds.items():
|
||||
if pd_identifier not in pds:
|
||||
logging.error(
|
||||
"Physical drive {} listed in virtual drive {} not "
|
||||
"found in drives list".format(
|
||||
pd_identifier, vd["vd_array"]
|
||||
)
|
||||
)
|
||||
continue
|
||||
pds[pd_identifier].setdefault("custom_fields", {}).update(vd)
|
||||
|
||||
return list(pds.values())
|
||||
|
||||
|
||||
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['Command Status']['Controller'],
|
||||
controller['Response Data']
|
||||
)
|
||||
controllers = storecli('/call show')
|
||||
for controller_id, controller_data in controllers.items():
|
||||
self.controllers.append(
|
||||
StorcliController(
|
||||
controller_id,
|
||||
controller_data
|
||||
)
|
||||
)
|
||||
|
||||
def get_controllers(self):
|
||||
return self.controllers
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
import logging
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from pprint import pprint
|
||||
|
||||
import netbox_agent.dmidecode as dmidecode
|
||||
from netbox_agent.config import config
|
||||
from netbox_agent.config import netbox_instance as nb
|
||||
|
|
@ -12,6 +6,11 @@ from netbox_agent.location import Datacenter, Rack, Tenant
|
|||
from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_type
|
||||
from netbox_agent.network import ServerNetwork
|
||||
from netbox_agent.power import PowerSupply
|
||||
from pprint import pprint
|
||||
import subprocess
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
class ServerBase():
|
||||
|
|
@ -28,9 +27,19 @@ class ServerBase():
|
|||
|
||||
self.network = None
|
||||
|
||||
self.tags = list(set(config.device.tags.split(','))) if config.device.tags else []
|
||||
if self.tags and len(self.tags):
|
||||
create_netbox_tags(self.tags)
|
||||
self.tags = list(set([
|
||||
x.strip() for x in config.device.tags.split(',') if x.strip()
|
||||
])) if config.device.tags else []
|
||||
self.nb_tags = list(create_netbox_tags(self.tags))
|
||||
config_cf = set([
|
||||
f.strip() for f in config.device.custom_fields.split(",")
|
||||
if f.strip()
|
||||
])
|
||||
self.custom_fields = {}
|
||||
self.custom_fields.update(dict([
|
||||
(k.strip(), v.strip()) for k, v in
|
||||
[f.split("=", 1) for f in config_cf]
|
||||
]))
|
||||
|
||||
def get_tenant(self):
|
||||
tenant = Tenant()
|
||||
|
|
@ -95,6 +104,19 @@ class ServerBase():
|
|||
server.position = None
|
||||
return update, server
|
||||
|
||||
def update_netbox_expansion_location(self, server, expansion):
|
||||
update = False
|
||||
if expansion.tenant != server.tenant:
|
||||
expansion.tenant = server.tenant
|
||||
update = True
|
||||
if expansion.site != server.site:
|
||||
expansion.site = server.site
|
||||
update = True
|
||||
if expansion.rack != server.rack:
|
||||
expansion.rack = server.rack
|
||||
update = True
|
||||
return update
|
||||
|
||||
def get_rack(self):
|
||||
rack = Rack()
|
||||
return rack.get()
|
||||
|
|
@ -181,7 +203,8 @@ class ServerBase():
|
|||
site=datacenter.id if datacenter else None,
|
||||
tenant=tenant.id if tenant else None,
|
||||
rack=rack.id if rack else None,
|
||||
tags=self.tags,
|
||||
tags=[{'name': x} for x in self.tags],
|
||||
custom_fields=self.custom_fields,
|
||||
)
|
||||
return new_chassis
|
||||
|
||||
|
|
@ -203,7 +226,8 @@ class ServerBase():
|
|||
site=datacenter.id if datacenter else None,
|
||||
tenant=tenant.id if tenant else None,
|
||||
rack=rack.id if rack else None,
|
||||
tags=self.tags,
|
||||
tags=[{'name': x} for x in self.tags],
|
||||
custom_fields=self.custom_fields,
|
||||
)
|
||||
return new_blade
|
||||
|
||||
|
|
@ -225,10 +249,17 @@ class ServerBase():
|
|||
site=datacenter.id if datacenter else None,
|
||||
tenant=tenant.id if tenant else None,
|
||||
rack=rack.id if rack else None,
|
||||
tags=self.tags,
|
||||
tags=[{'name': x} for x in self.tags],
|
||||
)
|
||||
return new_blade
|
||||
|
||||
def _netbox_deduplicate_server(self):
|
||||
serial = self.get_service_tag()
|
||||
hostname = self.get_hostname()
|
||||
server = nb.dcim.devices.get(name=hostname)
|
||||
if server and server.serial != serial:
|
||||
server.delete()
|
||||
|
||||
def _netbox_create_server(self, datacenter, tenant, rack):
|
||||
device_role = get_device_role(config.device.server_role)
|
||||
device_type = get_device_type(self.get_product_name())
|
||||
|
|
@ -246,17 +277,22 @@ class ServerBase():
|
|||
site=datacenter.id if datacenter else None,
|
||||
tenant=tenant.id if tenant else None,
|
||||
rack=rack.id if rack else None,
|
||||
tags=self.tags,
|
||||
tags=[{'name': x} for x in self.tags],
|
||||
)
|
||||
return new_server
|
||||
|
||||
def get_netbox_server(self):
|
||||
return nb.dcim.devices.get(serial=self.get_service_tag())
|
||||
def get_netbox_server(self, expansion=False):
|
||||
if expansion is False:
|
||||
return nb.dcim.devices.get(serial=self.get_service_tag())
|
||||
else:
|
||||
return nb.dcim.devices.get(serial=self.get_expansion_service_tag())
|
||||
|
||||
def _netbox_set_or_update_blade_slot(self, server, chassis, datacenter):
|
||||
# before everything check if right chassis
|
||||
actual_device_bay = server.parent_device.device_bay if server.parent_device else None
|
||||
actual_chassis = actual_device_bay.device if actual_device_bay else None
|
||||
actual_device_bay = server.parent_device.device_bay \
|
||||
if server.parent_device else None
|
||||
actual_chassis = actual_device_bay.device \
|
||||
if actual_device_bay else None
|
||||
slot = self.get_blade_slot()
|
||||
if actual_chassis and \
|
||||
actual_chassis.serial == chassis.serial and \
|
||||
|
|
@ -267,7 +303,11 @@ class ServerBase():
|
|||
device_id=chassis.id,
|
||||
name=slot,
|
||||
)
|
||||
if len(real_device_bays) > 0:
|
||||
real_device_bays = nb.dcim.device_bays.filter(
|
||||
device_id=chassis.id,
|
||||
name=slot,
|
||||
)
|
||||
if real_device_bays:
|
||||
logging.info(
|
||||
'Setting device ({serial}) new slot on {slot} '
|
||||
'(Chassis {chassis_serial})..'.format(
|
||||
|
|
@ -275,10 +315,14 @@ class ServerBase():
|
|||
))
|
||||
# reset actual device bay if set
|
||||
if actual_device_bay:
|
||||
# Forces the evaluation of the installed_device attribute to
|
||||
# workaround a bug probably due to lazy loading optimization
|
||||
# that prevents the value change detection
|
||||
actual_device_bay.installed_device
|
||||
actual_device_bay.installed_device = None
|
||||
actual_device_bay.save()
|
||||
# setup new device bay
|
||||
real_device_bay = real_device_bays[0]
|
||||
real_device_bay = next(real_device_bays)
|
||||
real_device_bay.installed_device = server
|
||||
real_device_bay.save()
|
||||
else:
|
||||
|
|
@ -286,9 +330,9 @@ class ServerBase():
|
|||
slot=slot
|
||||
))
|
||||
|
||||
def _netbox_set_or_update_blade_expansion_slot(self, server, chassis, datacenter):
|
||||
def _netbox_set_or_update_blade_expansion_slot(self, expansion, chassis, datacenter):
|
||||
# before everything check if right chassis
|
||||
actual_device_bay = server.parent_device.device_bay if server.parent_device else None
|
||||
actual_device_bay = expansion.parent_device.device_bay if expansion.parent_device else None
|
||||
actual_chassis = actual_device_bay.device if actual_device_bay else None
|
||||
slot = self.get_blade_expansion_slot()
|
||||
if actual_chassis and \
|
||||
|
|
@ -296,30 +340,32 @@ class ServerBase():
|
|||
actual_device_bay.name == slot:
|
||||
return
|
||||
|
||||
server.name += " expansion"
|
||||
|
||||
real_device_bays = nb.dcim.device_bays.filter(
|
||||
device_id=chassis.id,
|
||||
name=slot,
|
||||
)
|
||||
if len(real_device_bays) > 0:
|
||||
logging.info(
|
||||
'Setting device expansion ({serial}) new slot on {slot} '
|
||||
'(Chassis {chassis_serial})..'.format(
|
||||
serial=server.serial, slot=slot, chassis_serial=chassis.serial
|
||||
))
|
||||
# reset actual device bay if set
|
||||
if actual_device_bay:
|
||||
actual_device_bay.installed_device = None
|
||||
actual_device_bay.save()
|
||||
# setup new device bay
|
||||
real_device_bay = real_device_bays[0]
|
||||
real_device_bay.installed_device = server
|
||||
real_device_bay.save()
|
||||
else:
|
||||
if not real_device_bays:
|
||||
logging.error('Could not find slot {slot} expansion for chassis'.format(
|
||||
slot=slot
|
||||
))
|
||||
return
|
||||
logging.info(
|
||||
'Setting device expansion ({serial}) new slot on {slot} '
|
||||
'(Chassis {chassis_serial})..'.format(
|
||||
serial=expansion.serial, slot=slot, chassis_serial=chassis.serial
|
||||
))
|
||||
# reset actual device bay if set
|
||||
if actual_device_bay:
|
||||
# Forces the evaluation of the installed_device attribute to
|
||||
# workaround a bug probably due to lazy loading optimization
|
||||
# that prevents the value change detection
|
||||
actual_device_bay.installed_device
|
||||
actual_device_bay.installed_device = None
|
||||
actual_device_bay.save()
|
||||
# setup new device bay
|
||||
real_device_bay = next(real_device_bays)
|
||||
real_device_bay.installed_device = expansion
|
||||
real_device_bay.save()
|
||||
|
||||
def netbox_create_or_update(self, config):
|
||||
"""
|
||||
|
|
@ -337,6 +383,9 @@ class ServerBase():
|
|||
rack = self.get_netbox_rack()
|
||||
tenant = self.get_netbox_tenant()
|
||||
|
||||
if config.purge_old_devices:
|
||||
self._netbox_deduplicate_server()
|
||||
|
||||
if self.is_blade():
|
||||
chassis = nb.dcim.devices.get(
|
||||
serial=self.get_chassis_service_tag()
|
||||
|
|
@ -361,9 +410,11 @@ class ServerBase():
|
|||
if config.register or config.update_all or config.update_network:
|
||||
self.network = ServerNetwork(server=self)
|
||||
self.network.create_or_update_netbox_network_cards()
|
||||
update_inventory = config.inventory and (config.register or
|
||||
config.update_all or config.update_inventory)
|
||||
# update inventory if feature is enabled
|
||||
if config.inventory and (config.register or config.update_all or config.update_inventory):
|
||||
self.inventory = Inventory(server=self)
|
||||
self.inventory = Inventory(server=self)
|
||||
if update_inventory:
|
||||
self.inventory.create_or_update()
|
||||
# update psu
|
||||
if config.register or config.update_all or config.update_psu:
|
||||
|
|
@ -371,24 +422,35 @@ class ServerBase():
|
|||
self.power.create_or_update_power_supply()
|
||||
self.power.report_power_consumption()
|
||||
|
||||
if self.own_expansion_slot():
|
||||
expansion = nb.dcim.devices.get(serial=self.get_expansion_service_tag())
|
||||
if self.own_expansion_slot() and config.expansion_as_device:
|
||||
logging.debug('Update Server expansion...')
|
||||
expansion = nb.dcim.devices.get(serial=self.get_expansion_service_tag())
|
||||
if not expansion:
|
||||
expansion = self._netbox_create_blade_expansion(chassis, datacenter, tenant, rack)
|
||||
|
||||
# set slot for blade expansion
|
||||
self._netbox_set_or_update_blade_expansion_slot(expansion, chassis, datacenter)
|
||||
if update_inventory:
|
||||
# Updates expansion inventory
|
||||
inventory = Inventory(server=self, update_expansion=True)
|
||||
inventory.create_or_update()
|
||||
elif self.own_expansion_slot() and expansion:
|
||||
expansion.delete()
|
||||
expansion = None
|
||||
|
||||
update = 0
|
||||
# for every other specs
|
||||
# check hostname
|
||||
if server.name != self.get_hostname():
|
||||
update += 1
|
||||
server.name = self.get_hostname()
|
||||
update += 1
|
||||
|
||||
if sorted(set(server.tags)) != sorted(set(self.tags)):
|
||||
server.tags = self.tags
|
||||
if sorted(set([x.name for x in server.tags])) != sorted(set(self.tags)):
|
||||
server.tags = [x.id for x in self.nb_tags]
|
||||
update += 1
|
||||
|
||||
if server.custom_fields != self.custom_fields:
|
||||
server.custom_fields = self.custom_fields
|
||||
update += 1
|
||||
|
||||
if config.update_all or config.update_location:
|
||||
|
|
@ -397,6 +459,17 @@ class ServerBase():
|
|||
|
||||
if update:
|
||||
server.save()
|
||||
|
||||
if expansion:
|
||||
update = 0
|
||||
expansion_name = server.name + ' expansion'
|
||||
if expansion.name != expansion_name:
|
||||
expansion.name = expansion_name
|
||||
update += 1
|
||||
if self.update_netbox_expansion_location(server, expansion):
|
||||
update += 1
|
||||
if update:
|
||||
expansion.save()
|
||||
logging.debug('Finished updating Server!')
|
||||
|
||||
def print_debug(self):
|
||||
|
|
@ -414,3 +487,21 @@ class ServerBase():
|
|||
print('NIC:',)
|
||||
pprint(self.network.get_network_cards())
|
||||
pass
|
||||
|
||||
def own_expansion_slot(self):
|
||||
"""
|
||||
Indicates if the device hosts an expansion card
|
||||
"""
|
||||
return False
|
||||
|
||||
def own_gpu_expansion_slot(self):
|
||||
"""
|
||||
Indicates if the device hosts a GPU expansion card
|
||||
"""
|
||||
return False
|
||||
|
||||
def own_drive_expansion_slot(self):
|
||||
"""
|
||||
Indicates if the device hosts a drive expansion bay
|
||||
"""
|
||||
return False
|
||||
|
|
|
|||
7
netbox_agent/vendors/dell.py
vendored
7
netbox_agent/vendors/dell.py
vendored
|
|
@ -86,10 +86,3 @@ class DellHost(ServerBase):
|
|||
Expansion slot are always the compute bay number + 1
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def own_expansion_slot(self):
|
||||
"""
|
||||
Say if the device can host an extension card based
|
||||
on the product name
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
28
netbox_agent/vendors/generic.py
vendored
28
netbox_agent/vendors/generic.py
vendored
|
|
@ -8,7 +8,7 @@ class GenericHost(ServerBase):
|
|||
self.manufacturer = dmidecode.get_by_type(self.dmi, 'Baseboard')[0].get('Manufacturer')
|
||||
|
||||
def is_blade(self):
|
||||
return None
|
||||
return False
|
||||
|
||||
def get_blade_slot(self):
|
||||
return None
|
||||
|
|
@ -21,29 +21,3 @@ class GenericHost(ServerBase):
|
|||
|
||||
def get_chassis_service_tag(self):
|
||||
return self.get_service_tag()
|
||||
|
||||
def get_expansion_product(self):
|
||||
"""
|
||||
Get the extension slot that is on a pair slot number
|
||||
next to the compute slot that is on an odd slot number
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_expansion_slot(self, server):
|
||||
"""
|
||||
Return True if its an extension slot
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_blade_expansion_slot(self):
|
||||
"""
|
||||
Expansion slot are always the compute bay number + 1
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def own_expansion_slot(self):
|
||||
"""
|
||||
Say if the device can host an extension card based
|
||||
on the product name
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
59
netbox_agent/vendors/hp.py
vendored
59
netbox_agent/vendors/hp.py
vendored
|
|
@ -1,5 +1,6 @@
|
|||
import netbox_agent.dmidecode as dmidecode
|
||||
from netbox_agent.server import ServerBase
|
||||
from netbox_agent.inventory import Inventory
|
||||
|
||||
|
||||
class HPHost(ServerBase):
|
||||
|
|
@ -12,8 +13,8 @@ class HPHost(ServerBase):
|
|||
|
||||
def is_blade(self):
|
||||
blade = self.product.startswith("ProLiant BL")
|
||||
blade |= (self.product.startswith("ProLiant m")
|
||||
and self.product.endswith("Server Cartridge"))
|
||||
blade |= self.product.startswith("ProLiant m") and \
|
||||
self.product.endswith("Server Cartridge")
|
||||
return blade
|
||||
|
||||
def _find_rack_locator(self):
|
||||
|
|
@ -67,35 +68,53 @@ class HPHost(ServerBase):
|
|||
return self.hp_rack_locator["Enclosure Serial"].strip()
|
||||
return self.get_service_tag()
|
||||
|
||||
def get_blade_expansion_slot(self):
|
||||
"""
|
||||
Expansion slot are always the compute bay number + 1
|
||||
"""
|
||||
if self.is_blade() and self.own_gpu_expansion_slot() or \
|
||||
self.own_disk_expansion_slot() or True:
|
||||
return 'Bay {}'.format(
|
||||
str(int(self.hp_rack_locator['Server Bay'].strip()) + 1)
|
||||
)
|
||||
return None
|
||||
|
||||
def get_expansion_product(self):
|
||||
"""
|
||||
Get the extension slot that is on a pair slot number
|
||||
next to the compute slot that is on an odd slot number
|
||||
I only know on model of slot GPU extension card that.
|
||||
"""
|
||||
if self.own_expansion_slot():
|
||||
if self.own_gpu_expansion_slot():
|
||||
return "ProLiant BL460c Graphics Expansion Blade"
|
||||
return None
|
||||
|
||||
def is_expansion_slot(self, server):
|
||||
"""
|
||||
Return True if its an extension slot, based on the name
|
||||
"""
|
||||
return server.name.endswith(" expansion")
|
||||
|
||||
def get_blade_expansion_slot(self):
|
||||
"""
|
||||
Expansion slot are always the compute bay number + 1
|
||||
"""
|
||||
if self.is_blade() and self.own_expansion_slot():
|
||||
return 'Bay {}'.format(
|
||||
str(int(self.hp_rack_locator['Server Bay'].strip()) + 1)
|
||||
)
|
||||
elif self.own_disk_expansion_slot():
|
||||
return "ProLiant BL460c Disk Expansion Blade"
|
||||
return None
|
||||
|
||||
def own_expansion_slot(self):
|
||||
"""
|
||||
Say if the device can host an extension card based
|
||||
Indicates if the device hosts an expension card
|
||||
"""
|
||||
return self.own_gpu_expansion_slot() or self.own_disk_expansion_slot()
|
||||
|
||||
def own_gpu_expansion_slot(self):
|
||||
"""
|
||||
Indicates if the device hosts a GPU expansion card based
|
||||
on the product name
|
||||
"""
|
||||
return self.get_product_name().endswith('Graphics Exp')
|
||||
|
||||
def own_disk_expansion_slot(self):
|
||||
"""
|
||||
Indicates if the device hosts a drive expansion card based
|
||||
on raid card attributes.
|
||||
"""
|
||||
# Uses already parsed inventory if available
|
||||
# parses it otherwise
|
||||
inventory = getattr(self, "inventory", None)
|
||||
if inventory is None:
|
||||
inventory = Inventory(self)
|
||||
for raid_card in inventory.get_raid_cards():
|
||||
if self.is_blade() and raid_card.is_external():
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
19
netbox_agent/vendors/supermicro.py
vendored
19
netbox_agent/vendors/supermicro.py
vendored
|
|
@ -79,22 +79,3 @@ class SupermicroHost(ServerBase):
|
|||
I only know on model of slot GPU extension card that.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_expansion_slot(self, server):
|
||||
"""
|
||||
Return True if its an extension slot, based on the name
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_blade_expansion_slot(self):
|
||||
"""
|
||||
Expansion slot are always the compute bay number + 1
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def own_expansion_slot(self):
|
||||
"""
|
||||
Say if the device can host an extension card based
|
||||
on the product name
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
editor.link_modal.header
Reference in a new issue