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 from netbox_agent.inventory import Inventory from netbox_agent.location import Datacenter, Rack, Tenant from netbox_agent.misc import ( create_netbox_tags, get_device_platform, get_device_role, get_device_type, ) from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply class ServerBase: def __init__(self, dmi=None): if dmi: self.dmi = dmi else: self.dmi = dmidecode.parse() self.baseboard = dmidecode.get_by_type(self.dmi, "Baseboard") self.bios = dmidecode.get_by_type(self.dmi, "BIOS") self.chassis = dmidecode.get_by_type(self.dmi, "Chassis") self.system = dmidecode.get_by_type(self.dmi, "System") self.device_platform = get_device_platform(config.device.platform) self.network = None 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() return tenant.get() def get_netbox_tenant(self): tenant = self.get_tenant() if tenant is None: return None nb_tenant = nb.tenancy.tenants.get(slug=self.get_tenant()) return nb_tenant def get_datacenter(self): dc = Datacenter() return dc.get() def get_netbox_datacenter(self): dc = self.get_datacenter() if dc is None: logging.error("Specifying a datacenter (Site) is mandatory in Netbox") sys.exit(1) nb_dc = nb.dcim.sites.get( slug=dc, ) if nb_dc is None: logging.error("Site (slug: {}) has not been found".format(dc)) sys.exit(1) return nb_dc def update_netbox_location(self, server): dc = self.get_datacenter() nb_rack = self.get_netbox_rack() nb_dc = self.get_netbox_datacenter() update = False if dc and server.site and server.site.slug != nb_dc.slug: logging.info( "Datacenter location has changed from {} to {}, updating".format( server.site.slug, nb_dc.slug, ) ) update = True server.site = nb_dc.id if server.rack and nb_rack and server.rack.id != nb_rack.id: logging.info( "Rack location has changed from {} to {}, updating".format( server.rack, nb_rack, ) ) update = True server.rack = nb_rack if nb_rack is None: server.face = None 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() def get_netbox_rack(self): rack = self.get_rack() datacenter = self.get_netbox_datacenter() if not rack: return None if rack and not datacenter: logging.error("Can't get rack if no datacenter is configured or found") sys.exit(1) return nb.dcim.racks.get( name=rack, site_id=datacenter.id, ) def get_product_name(self): """ Return the Chassis Name from dmidecode info """ return self.system[0]["Product Name"].strip() def get_service_tag(self): """ Return the Service Tag from dmidecode info """ return self.system[0]["Serial Number"].strip() def get_expansion_service_tag(self): """ Return the virtual Service Tag from dmidecode info host with 'expansion' """ return self.system[0]["Serial Number"].strip() + " expansion" def get_hostname(self): if config.hostname_cmd is None: return "{}".format(socket.gethostname()) return subprocess.getoutput(config.hostname_cmd) def is_blade(self): raise NotImplementedError def get_blade_slot(self): raise NotImplementedError def get_chassis(self): raise NotImplementedError def get_chassis_name(self): raise NotImplementedError def get_chassis_service_tag(self): raise NotImplementedError def get_bios_version(self): raise NotImplementedError def get_bios_version_attr(self): raise NotImplementedError def get_bios_release_date(self): raise NotImplementedError def get_power_consumption(self): raise NotImplementedError def get_expansion_product(self): raise NotImplementedError def _netbox_create_chassis(self, datacenter, tenant, rack): device_type = get_device_type(self.get_chassis()) device_role = get_device_role(config.device.chassis_role) serial = self.get_chassis_service_tag() logging.info("Creating chassis blade (serial: {serial})".format(serial=serial)) new_chassis = nb.dcim.devices.create( name=self.get_chassis_name(), device_type=device_type.id, serial=serial, role=device_role.id, site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, tags=[{"name": x} for x in self.tags], custom_fields=self.custom_fields, ) return new_chassis def _netbox_create_blade(self, chassis, datacenter, tenant, rack): device_role = get_device_role(config.device.blade_role) device_type = get_device_type(self.get_product_name()) serial = self.get_service_tag() hostname = self.get_hostname() logging.info( "Creating blade (serial: {serial}) {hostname} on chassis {chassis_serial}".format( serial=serial, hostname=hostname, chassis_serial=chassis.serial ) ) new_blade = nb.dcim.devices.create( name=hostname, serial=serial, role=device_role.id, device_type=device_type.id, parent_device=chassis.id, site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, tags=[{"name": x} for x in self.tags], custom_fields=self.custom_fields, ) return new_blade def _netbox_create_blade_expansion(self, chassis, datacenter, tenant, rack): device_role = get_device_role(config.device.blade_role) device_type = get_device_type(self.get_expansion_product()) serial = self.get_expansion_service_tag() hostname = self.get_hostname() + " expansion" logging.info( "Creating expansion (serial: {serial}) {hostname} on chassis {chassis_serial}".format( serial=serial, hostname=hostname, chassis_serial=chassis.serial ) ) new_blade = nb.dcim.devices.create( name=hostname, serial=serial, role=device_role.id, device_type=device_type.id, parent_device=chassis.id, site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, 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()) if not device_type: raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) serial = self.get_service_tag() hostname = self.get_hostname() logging.info( "Creating server (serial: {serial}) {hostname}".format( serial=serial, hostname=hostname ) ) new_server = nb.dcim.devices.create( name=hostname, serial=serial, role=device_role.id, device_type=device_type.id, platform=self.device_platform.id, site=datacenter.id if datacenter else None, tenant=tenant.id if tenant else None, rack=rack.id if rack else None, tags=[{"name": x} for x in self.tags], ) return new_server 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 slot = self.get_blade_slot() if ( actual_chassis and actual_chassis.serial == chassis.serial and actual_device_bay.name == slot ): return real_device_bays = nb.dcim.device_bays.filter( device_id=chassis.id, name=slot, ) 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( serial=server.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 = server real_device_bay.save() else: logging.error("Could not find slot {slot} for chassis".format(slot=slot)) def _netbox_set_or_update_blade_expansion_slot( self, expansion, chassis, datacenter ): # before everything check if right chassis 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 actual_chassis.serial == chassis.serial and actual_device_bay.name == slot ): return real_device_bays = nb.dcim.device_bays.filter( device_id=chassis.id, name=slot, ) 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): """ Netbox method to create or update info about our server/blade Handle: * new chassis for a blade * new slot for a blade * hostname update * Network infos * Inventory management * PSU management """ datacenter = self.get_netbox_datacenter() 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()) # Chassis does not exist if not chassis: chassis = self._netbox_create_chassis(datacenter, tenant, rack) server = nb.dcim.devices.get(serial=self.get_service_tag()) if not server: server = self._netbox_create_blade(chassis, datacenter, tenant, rack) # Set slot for blade self._netbox_set_or_update_blade_slot(server, chassis, datacenter) else: server = nb.dcim.devices.get(serial=self.get_service_tag()) if not server: server = self._netbox_create_server(datacenter, tenant, rack) logging.debug("Updating Server...") # check network cards 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 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: self.power = PowerSupply(server=self) self.power.create_or_update_power_supply() self.power.report_power_consumption() 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...") 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(): server.name = self.get_hostname() update += 1 server_tags = sorted(set([x.name for x in server.tags])) tags = sorted(set(self.tags)) if server_tags != tags: new_tags_ids = [x.id for x in self.nb_tags] if not config.preserve_tags: server.tags = new_tags_ids else: server_tags_ids = [x.id for x in server.tags] server.tags = sorted(set(new_tags_ids + server_tags_ids)) 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: ret, server = self.update_netbox_location(server) update += ret if server.platform != self.device_platform: server.platform = self.device_platform update += 1 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): self.network = ServerNetwork(server=self) print("Datacenter:", self.get_datacenter()) print("Netbox Datacenter:", self.get_netbox_datacenter()) print("Rack:", self.get_rack()) print("Netbox Rack:", self.get_netbox_rack()) print("Is blade:", self.is_blade()) print("Got expansion:", self.own_expansion_slot()) print("Product Name:", self.get_product_name()) print("Platform:", self.device_platform) print("Chassis:", self.get_chassis()) print("Chassis service tag:", self.get_chassis_service_tag()) print("Service tag:", self.get_service_tag()) 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