diff --git a/README.md b/README.md index c6a3252..e7ccd2a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The goal is to generate an existing infrastructure on Netbox and have the abilit # Features -* Create servers, chassis and blade through standard tools (`dmidecode`) +* Create virtual machines, servers, chassis and blade through standard tools (`dmidecode`) * Create physical, bonding and vlan network interfaces with IPs (IPv4 & IPv6) * Create IPMI interface if found * Create or get existing VLAN and associate it to interfaces @@ -52,6 +52,12 @@ network: # enable auto-cabling lldp: true +# virtual: +# # not mandatory, can be guessed +# enabled: True +# # see https://netbox.company.com/virtualization/clusters/ +# cluster_name: my_vm_cluster + datacenter_location: driver: "cmd:cat /etc/qualification | tr [a-z] [A-Z]" regex: "DATACENTER: (?P[A-Za-z0-9]+)" @@ -94,6 +100,14 @@ In order to handle this case, user need to set Netbox IP's mode to `Anycast` so Tested on: +## Virtual Machines + +* Hyper-V +* VMWare +* VirtualBox +* AWS +* GCP + ## Dell Inc. ### Blades diff --git a/netbox_agent/cli.py b/netbox_agent/cli.py index 3621828..5777f1a 100644 --- a/netbox_agent/cli.py +++ b/netbox_agent/cli.py @@ -6,6 +6,7 @@ from netbox_agent.vendors.generic import GenericHost from netbox_agent.vendors.hp import HPHost from netbox_agent.vendors.qct import QCTHost from netbox_agent.vendors.supermicro import SupermicroHost +from netbox_agent.virtualmachine import VirtualMachine, is_vm MANUFACTURERS = { 'Dell Inc.': DellHost, @@ -19,20 +20,23 @@ MANUFACTURERS = { def run(config): dmi = dmidecode.parse() - manufacturer = dmidecode.get_by_type(dmi, 'Chassis')[0].get('Manufacturer') - try: - server = MANUFACTURERS[manufacturer](dmi=dmi) - except KeyError: - server = GenericHost(dmi=dmi) + if config.virtual.enabled or is_vm(dmi): + if not config.virtual.cluster_name: + raise Exception('virtual.cluster_name parameter is mandatory because it\'s a VM') + server = VirtualMachine(dmi=dmi) + else: + manufacturer = dmidecode.get_by_type(dmi, 'Chassis')[0].get('Manufacturer') + try: + server = MANUFACTURERS[manufacturer](dmi=dmi) + except KeyError: + server = GenericHost(dmi=dmi) if config.debug: server.print_debug() - if config.register: - server.netbox_create(config) - if config.update_all or config.update_network or config.update_location or \ + 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_update(config) + server.netbox_create_or_update(config) return True diff --git a/netbox_agent/config.py b/netbox_agent/config.py index 4626fbd..30d4675 100644 --- a/netbox_agent/config.py +++ b/netbox_agent/config.py @@ -30,6 +30,8 @@ def get_config(): p.add_argument('--log_level', default='debug') p.add_argument('--netbox.url', help='Netbox URL') p.add_argument('--netbox.token', help='Netbox API Token') + p.add_argument('--virtual.enabled', action='store_true', help='Is a virtual machine or not') + p.add_argument('--virtual.cluster_name', help='Cluster name of VM') p.add_argument('--hostname_cmd', default=None, help="Command to output hostname, used as Device's name in netbox") p.add_argument('--datacenter_location.driver', diff --git a/netbox_agent/ethtool.py b/netbox_agent/ethtool.py index a58ef50..132cd09 100644 --- a/netbox_agent/ethtool.py +++ b/netbox_agent/ethtool.py @@ -65,11 +65,11 @@ class Ethtool(): def _parse_ethtool_module_output(self): status, output = subprocess.getstatusoutput('ethtool -m {}'.format(self.interface)) - if status != 0: - return {} - r = re.search(r'Identifier.*\((\w+)\)', output) - if r and len(r.groups()) > 0: - return {'form_factor': r.groups()[0]} + if status == 0: + r = re.search(r'Identifier.*\((\w+)\)', output) + if r and len(r.groups()) > 0: + return {'form_factor': r.groups()[0]} + return {} def parse(self): if which('ethtool') is None: diff --git a/netbox_agent/inventory.py b/netbox_agent/inventory.py index 9622fb2..8321bba 100644 --- a/netbox_agent/inventory.py +++ b/netbox_agent/inventory.py @@ -438,18 +438,7 @@ class Inventory(): if memory.get('serial') not in [x.serial for x in nb_memories]: self.create_netbox_memory(memory) - def create(self): - if config.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() - return True - - def update(self): + def create_or_update(self): if config.inventory is None or config.update_inventory is None: return False self.do_netbox_cpus() diff --git a/netbox_agent/ipmi.py b/netbox_agent/ipmi.py index 380ead4..a0c9df6 100644 --- a/netbox_agent/ipmi.py +++ b/netbox_agent/ipmi.py @@ -1,6 +1,8 @@ import logging import subprocess +from netaddr import IPNetwork + class IPMI(): """ @@ -45,12 +47,27 @@ class IPMI(): logging.error('Cannot get ipmi info: {}'.format(self.output)) def parse(self): + _ipmi = {} if self.ret != 0: - return None + return _ipmi - ret = {} for line in self.output.splitlines(): key = line.split(':')[0].strip() + if key not in ['802.1q VLAN ID', 'IP Address', 'Subnet Mask', 'MAC Address']: + continue value = ':'.join(line.split(':')[1:]).strip() - ret[key] = value + _ipmi[key] = value + + ret = {} + ret['name'] = 'IPMI' + ret['bonding'] = False + ret['mac'] = _ipmi['MAC Address'] + ret['vlan'] = int(_ipmi['802.1q VLAN ID']) \ + if _ipmi['802.1q VLAN ID'] != 'Disabled' else None + ip = _ipmi['IP Address'] + netmask = _ipmi['Subnet Mask'] + address = str(IPNetwork('{}/{}'.format(ip, netmask))) + + ret['ip'] = [address] + ret['ipmi'] = True return ret diff --git a/netbox_agent/logging.py b/netbox_agent/logging.py index 8767feb..a0f7e20 100644 --- a/netbox_agent/logging.py +++ b/netbox_agent/logging.py @@ -3,8 +3,7 @@ import logging from netbox_agent.config import config logger = logging.getLogger() - -if config.log_level == 'debug': +if config.log_level.lower() == 'debug': logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) diff --git a/netbox_agent/misc.py b/netbox_agent/misc.py index bf0922e..cc1873c 100644 --- a/netbox_agent/misc.py +++ b/netbox_agent/misc.py @@ -1,3 +1,5 @@ +import socket +import subprocess from shutil import which @@ -29,3 +31,9 @@ def get_vendor(name): if name.upper().startswith(key): return value return name + + +def get_hostname(config): + if config.hostname_cmd is None: + return '{}'.format(socket.gethostname()) + return subprocess.getoutput(config.hostname_cmd) diff --git a/netbox_agent/network.py b/netbox_agent/network.py index b3461c6..4005f98 100644 --- a/netbox_agent/network.py +++ b/netbox_agent/network.py @@ -4,7 +4,7 @@ import re from itertools import chain import netifaces -from netaddr import IPAddress, IPNetwork +from netaddr import IPAddress from netbox_agent.config import config from netbox_agent.config import netbox_instance as nb @@ -13,15 +13,13 @@ from netbox_agent.ipmi import IPMI from netbox_agent.lldp import LLDP -class Network(): +class Network(object): def __init__(self, server, *args, **kwargs): self.nics = [] - self.server = server - self.device = self.server.get_netbox_server() self.lldp = LLDP() if config.network.lldp else None - self.scan() - + self.nics = self.scan() + self.ipmi = None self.dcim_choices = {} dcim_c = nb.dcim.choices() @@ -38,7 +36,11 @@ class Network(): for c in ipam_c[choice]: self.ipam_choices[choice][c['label']] = c['value'] + def get_network_type(): + return NotImplementedError + def scan(self): + nics = [] for interface in os.listdir('/sys/class/net/'): # ignore if it's not a link (ie: bonding_masters etc) if not os.path.islink('/sys/class/net/{}'.format(interface)): @@ -102,7 +104,8 @@ class Network(): 'bonding': bonding, 'bonding_slaves': bonding_slaves, } - self.nics.append(nic) + nics.append(nic) + return nics def _set_bonding_interfaces(self): bonding_nics = (x for x in self.nics if x['bonding']) @@ -130,25 +133,30 @@ class Network(): def get_netbox_network_card(self, nic): if nic['mac'] is None: - interface = nb.dcim.interfaces.get( - device_id=self.device.id, + interface = self.nb_net.interfaces.get( name=nic['name'], + **self.custom_arg_id, ) else: - interface = nb.dcim.interfaces.get( - device_id=self.device.id, + interface = self.nb_net.interfaces.get( mac_address=nic['mac'], name=nic['name'], + **self.custom_arg_id, ) return interface def get_netbox_network_cards(self): - return nb.dcim.interfaces.filter( - device_id=self.device.id, - mgmt_only=False, + return self.nb_net.interfaces.filter( + **self.custom_arg_id, ) def get_netbox_type_for_nic(self, nic): + if self.get_network_type() == 'virtual': + return self.dcim_choices['interface:type']['Virtual'] + + if nic.get('bonding'): + return self.dcim_choices['interface:type']['Link Aggregation Group (LAG)'] + if nic.get('bonding'): return self.dcim_choices['interface:type']['Link Aggregation Group (LAG)'] if nic.get('ethtool') is None: @@ -165,17 +173,6 @@ class Network(): return self.dcim_choices['interface:type']['1000BASE-T (1GE)'] return self.dcim_choices['interface:type']['Other'] - def get_ipmi(self): - ipmi = IPMI().parse() - return ipmi - - def get_netbox_ipmi(self): - ipmi = self.get_ipmi() - mac = ipmi['MAC Address'] - return nb.dcim.interfaces.get( - mac=mac - ) - def get_or_create_vlan(self, vlan_id): # FIXME: we may need to specify the datacenter # since users may have same vlan id in multiple dc @@ -206,9 +203,10 @@ class Network(): # if it's a vlan interface elif vlan_id and ( interface.mode is None or - interface.mode.value != self.dcim_choices['interface:mode']['Access'] or - len(interface.tagged_vlans) != 1 or - interface.tagged_vlans[0].vid != vlan_id): + type(interface.mode) is not int and ( + interface.mode.value == self.dcim_choices['interface:mode']['Access'] or + len(interface.tagged_vlans) != 1 or + interface.tagged_vlans[0].vid != vlan_id)): logging.info('Resetting tagged VLAN(s) on interface {interface}'.format( interface=interface)) update = True @@ -232,45 +230,6 @@ class Network(): interface.untagged_vlan = nb_vlan.id return update, interface - def create_or_update_ipmi(self): - ipmi = self.get_ipmi() - if ipmi is None: - return None - - mac = ipmi['MAC Address'] - ip = ipmi['IP Address'] - netmask = ipmi['Subnet Mask'] - vlan = int(ipmi['802.1q VLAN ID']) if ipmi['802.1q VLAN ID'] != 'Disabled' else None - address = str(IPNetwork('{}/{}'.format(ip, netmask))) - - interface = nb.dcim.interfaces.get( - device_id=self.device.id, - mgmt_only=True, - ) - nic = { - 'name': 'IPMI', - 'mac': mac, - 'vlan': vlan, - 'ip': [address], - } - if interface is None: - interface = self.create_netbox_nic(nic, mgmt=True) - self.create_or_update_netbox_ip_on_interface(address, interface) - else: - # let the user chose the name of mgmt ? - # guess it with manufacturer (IDRAC, ILO, ...) ? - update = False - self.create_or_update_netbox_ip_on_interface(address, interface) - update, interface = self.reset_vlan_on_interface(nic, interface) - if mac.upper() != interface.mac_address: - logging.info('IPMI mac changed from {old_mac} to {new_mac}'.format( - old_mac=interface.mac_address, new_mac=mac.upper())) - interface.mac_address = mac - update = True - if update: - interface.save() - return interface - def create_netbox_nic(self, nic, mgmt=False): # TODO: add Optic Vendor, PN and Serial type = self.get_netbox_type_for_nic(nic) @@ -278,12 +237,12 @@ class Network(): name=nic['name'], mac=nic['mac'], device=self.device.name)) nb_vlan = None - interface = nb.dcim.interfaces.create( - device=self.device.id, + interface = self.nb_net.interfaces.create( name=nic['name'], mac_address=nic['mac'], type=type, mgmt_only=mgmt, + **self.custom_arg, ) if nic['vlan']: @@ -383,6 +342,112 @@ class Network(): netbox_ip.save() return netbox_ip + def create_or_update_netbox_network_cards(self): + if config.update_all is None or config.update_network is None: + return None + logging.debug('Creating/Updating NIC...') + + # delete unknown interface + nb_nics = self.get_netbox_network_cards() + local_nics = [x['name'] for x in self.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 + )) + nb_nics.remove(nic) + nic.delete() + + # 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], + ) + 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 + netbox_ip.save() + + # update each nic + for nic in self.nics: + interface = self.get_netbox_network_card(nic) + if not interface: + logging.info('Interface {mac_address} not found, creating..'.format( + mac_address=nic['mac']) + ) + interface = self.create_netbox_nic(nic) + + nic_update = 0 + if nic['name'] != interface.name: + logging.info('Updating interface {interface} name to: {name}'.format( + interface=interface, name=nic['name'])) + interface.name = nic['name'] + nic_update += 1 + + 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, 'lag') and interface.lag is not None: + local_lag_int = next( + item for item in self.nics if item['name'] == interface.lag.name + ) + if nic['name'] not in local_lag_int['bonding_slaves']: + logging.info('Interface has no LAG, resetting') + nic_update += 1 + interface.lag = None + + # cable the interface + if config.network.lldp: + switch_ip = self.lldp.get_switch_ip(interface.name) + switch_interface = self.lldp.get_switch_port(interface.name) + if switch_ip and switch_interface: + ret, interface = self.create_or_update_cable( + switch_ip, switch_interface, interface + ) + nic_update += ret + + if nic['ip']: + # sync local IPs + for ip in nic['ip']: + self.create_or_update_netbox_ip_on_interface(ip, interface) + if nic_update > 0: + interface.save() + + self._set_bonding_interfaces() + logging.debug('Finished updating NIC!') + + +class ServerNetwork(Network): + def __init__(self, server, *args, **kwargs): + super(ServerNetwork, self).__init__(server, args, kwargs) + self.ipmi = self.get_ipmi() + if self.ipmi: + self.nics.append(self.ipmi) + self.server = server + self.device = self.server.get_netbox_server() + self.nb_net = nb.dcim + self.custom_arg = {'device': self.device.id} + self.custom_arg_id = {'device_id': self.device.id} + + def get_network_type(self): + return 'server' + + def get_ipmi(self): + ipmi = IPMI().parse() + return ipmi + def connect_interface_to_switch(self, switch_ip, switch_interface, nb_server_interface): logging.info('Interface {} is not connected to switch, trying to connect..'.format( nb_server_interface.name @@ -486,105 +551,22 @@ class Network(): ) return update, nb_server_interface - def create_netbox_network_cards(self): - logging.debug('Creating NIC...') - for nic in self.nics: - interface = self.get_netbox_network_card(nic) - # if network doesn't exist we create it - if not interface: - new_interface = self.create_netbox_nic(nic) - if nic['ip']: - # for each ip, we try to find it - # assign the device's interface to it - # or simply create it - for ip in nic['ip']: - self.create_or_update_netbox_ip_on_interface(ip, new_interface) - self._set_bonding_interfaces() - self.create_or_update_ipmi() - logging.debug('Finished creating NIC!') - def update_netbox_network_cards(self): - if config.update_all is None or config.update_network is None: - return None - logging.debug('Updating NIC...') +class VirtualNetwork(Network): + def __init__(self, server, *args, **kwargs): + super(VirtualNetwork, self).__init__(server, args, kwargs) + self.server = server + self.device = self.server.get_netbox_vm() + self.nb_net = nb.virtualization + self.custom_arg = {'virtual_machine': self.device.id} + self.custom_arg_id = {'virtual_machine_id': self.device.id} - # delete unknown interface - nb_nics = self.get_netbox_network_cards() - local_nics = [x['name'] for x in self.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 - )) - nic.delete() + dcim_c = nb.virtualization.choices() - # delete IP on netbox that are not known on this server - netbox_ips = nb.ipam.ip_addresses.filter( - device_id=self.device.id, - interface_id=[x.id for x in nb_nics], - ) - 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 - netbox_ip.save() + for choice in dcim_c: + self.dcim_choices[choice] = {} + for c in dcim_c[choice]: + self.dcim_choices[choice][c['label']] = c['value'] - # update each nic - for nic in self.nics: - interface = self.get_netbox_network_card(nic) - if not interface: - logging.info('Interface {mac_address} not found, creating..'.format( - mac_address=nic['mac']) - ) - interface = self.create_netbox_nic(nic) - - nic_update = 0 - if nic['name'] != interface.name: - logging.info('Updating interface {interface} name to: {name}'.format( - interface=interface, name=nic['name'])) - interface.name = nic['name'] - nic_update += 1 - - 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 interface.lag is not None: - local_lag_int = next( - item for item in self.nics if item['name'] == interface.lag.name - ) - if nic['name'] not in local_lag_int['bonding_slaves']: - logging.info('Interface has no LAG, resetting') - nic_update += 1 - interface.lag = None - - # cable the interface - if config.network.lldp: - switch_ip = self.lldp.get_switch_ip(interface.name) - switch_interface = self.lldp.get_switch_port(interface.name) - if switch_ip and switch_interface: - ret, interface = self.create_or_update_cable( - switch_ip, switch_interface, interface - ) - nic_update += ret - - if nic['ip']: - # sync local IPs - for ip in nic['ip']: - self.create_or_update_netbox_ip_on_interface(ip, interface) - if nic_update > 0: - interface.save() - - self._set_bonding_interfaces() - self.create_or_update_ipmi() - logging.debug('Finished updating NIC!') + def get_network_type(self): + return 'virtual' diff --git a/netbox_agent/server.py b/netbox_agent/server.py index ffda9a0..5db5513 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -8,7 +8,7 @@ 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 -from netbox_agent.network import Network +from netbox_agent.network import ServerNetwork from netbox_agent.power import PowerSupply @@ -136,7 +136,7 @@ class ServerBase(): def get_power_consumption(self): raise NotImplementedError - def _netbox_create_blade_chassis(self, datacenter, rack): + def _netbox_create_chassis(self, datacenter, rack): device_type = get_device_type(self.get_chassis()) device_role = get_device_role('Server Chassis') serial = self.get_chassis_service_tag() @@ -172,27 +172,6 @@ class ServerBase(): ) return new_blade - def _netbox_set_blade_slot(self, chassis, server): - slot = self.get_blade_slot() - # Find the slot and update it with our blade - device_bays = nb.dcim.device_bays.filter( - device_id=chassis.id, - name=slot, - ) - if len(device_bays) > 0: - logging.info( - 'Setting device ({serial}) new slot on {slot} ' - '(Chassis {chassis_serial})..'.format( - serial=server.serial, slot=slot, chassis_serial=chassis.serial - )) - device_bay = device_bays[0] - device_bay.installed_device = server - device_bay.save() - else: - logging.error('Could not find slot {slot} for chassis'.format( - slot=slot - )) - def _netbox_create_server(self, datacenter, rack): device_role = get_device_role('Server') device_type = get_device_type(self.get_product_name()) @@ -215,108 +194,89 @@ class ServerBase(): def get_netbox_server(self): return nb.dcim.devices.get(serial=self.get_service_tag()) - def netbox_create(self, config): - logging.debug('Creating Server..') + 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, + ) + if len(real_device_bays) > 0: + 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: + 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: + logging.error('Could not find slot {slot} for chassis'.format( + slot=slot + )) + + 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() - if self.is_blade(): - # let's find the blade - serial = self.get_service_tag() - blade = nb.dcim.devices.get(serial=serial) - chassis = nb.dcim.devices.get(serial=self.get_chassis_service_tag()) - # if it doesn't exist, create it - if not blade: - # check if the chassis exist before - # if it doesn't exist, create it - chassis = nb.dcim.devices.get( - serial=self.get_chassis_service_tag() - ) - if not chassis: - chassis = self._netbox_create_blade_chassis(datacenter, rack) - blade = self._netbox_create_blade(chassis, datacenter, rack) + 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, rack) + + server = nb.dcim.devices.get(serial=self.get_service_tag()) + if not server: + server = self._netbox_create_blade(chassis, datacenter, rack) # Set slot for blade - self._netbox_set_blade_slot(chassis, 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: self._netbox_create_server(datacenter, rack) - self.network = Network(server=self) - self.network.create_netbox_network_cards() - - self.power = PowerSupply(server=self) - self.power.create_or_update_power_supply() - - if config.inventory: - self.inventory = Inventory(server=self) - self.inventory.create() - logging.debug('Server created!') - - def _netbox_update_chassis_for_blade(self, server, datacenter): - chassis = server.parent_device.device_bay.device - device_bay = nb.dcim.device_bays.get( - server.parent_device.device_bay.id - ) - - parent_chassis = nb.dcim.devices.get( - chassis.id - ) - - netbox_chassis_serial = parent_chassis.serial - move_device_bay = False - - # check chassis serial with dmidecode - if netbox_chassis_serial != self.get_chassis_service_tag(): - move_device_bay = True - # try to find the new netbox chassis - chassis = nb.dcim.devices.get( - serial=self.get_chassis_service_tag() - ) - if not chassis: - chassis = self._netbox_create_blade_chassis(datacenter) - if move_device_bay or device_bay.name != self.get_blade_slot(): - logging.info('Device ({serial}) seems to have moved, reseting old slot..'.format( - serial=server.serial)) - device_bay.installed_device = None - device_bay.save() - - # Set slot for blade - self._netbox_set_blade_slot(chassis, server) - - def netbox_update(self, config): - """ - Netbox method to update info about our server/blade - - Handle: - * new chasis for a blade - * new slot for a bblade - * hostname update - * new network infos - """ 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 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.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() - server = nb.dcim.devices.get(serial=self.get_service_tag()) - if not server: - raise Exception("The server (Serial: {}) isn't yet registered in Netbox, register" - 'it before updating it'.format(self.get_service_tag())) update = 0 - if self.is_blade(): - datacenter = self.get_netbox_datacenter() - # if it's already linked to a chassis - if server.parent_device: - self._netbox_update_chassis_for_blade(server, datacenter) - else: - logging.info('Blade is not in a chassis, fixing...') - chassis = nb.dcim.devices.get( - serial=self.get_chassis_service_tag() - ) - if not chassis: - chassis = self._netbox_create_blade_chassis(datacenter) - # Set slot for blade - self._netbox_set_blade_slot(chassis, server) - # for every other specs # check hostname if server.name != self.get_hostname(): @@ -327,25 +287,12 @@ class ServerBase(): ret, server = self.update_netbox_location(server) update += ret - # check network cards - if config.update_all or config.update_network: - self.network = Network(server=self) - self.network.update_netbox_network_cards() - # update inventory - if config.update_all or config.update_inventory: - self.inventory = Inventory(server=self) - self.inventory.update() - # update psu - if config.update_all or config.update_psu: - self.power = PowerSupply(server=self) - self.power.create_or_update_power_supply() - self.power.report_power_consumption() if update: server.save() logging.debug('Finished updating Server!') def print_debug(self): - self.network = Network(server=self) + self.network = ServerNetwork(server=self) print('Datacenter:', self.get_datacenter()) print('Netbox Datacenter:', self.get_netbox_datacenter()) print('Rack:', self.get_rack()) diff --git a/netbox_agent/virtualmachine.py b/netbox_agent/virtualmachine.py new file mode 100644 index 0000000..1620ba0 --- /dev/null +++ b/netbox_agent/virtualmachine.py @@ -0,0 +1,86 @@ +import os + +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.misc import get_hostname +from netbox_agent.network import VirtualNetwork + + +def is_vm(dmi): + bios = dmidecode.get_by_type(dmi, 'BIOS') + system = dmidecode.get_by_type(dmi, 'System') + + if 'Hyper-V' in bios[0]['Version'] or \ + 'Xen' in bios[0]['Version'] or \ + 'Google Compute Engine' in system[0]['Product Name'] or \ + 'VirtualBox' in bios[0]['Version'] or \ + 'VMware' in system[0]['Manufacturer']: + return True + return False + + +class VirtualMachine(object): + def __init__(self, dmi=None): + if dmi: + self.dmi = dmi + else: + self.dmi = dmidecode.parse() + self.network = None + + def get_memory(self): + mem_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') # e.g. 4015976448 + mem_gib = mem_bytes / (1024.**2) # e.g. 3.74 + return int(mem_gib) + + def get_vcpus(self): + return os.cpu_count() + + def get_netbox_vm(self): + hostname = get_hostname(config) + vm = nb.virtualization.virtual_machines.get( + name=hostname + ) + return vm + + def get_netbox_cluster(self, name): + cluster = nb.virtualization.clusters.get( + name=name, + ) + return cluster + + def netbox_create_or_update(self, config): + logging.debug('It\'s a virtual machine') + created = False + updated = 0 + + hostname = get_hostname(config) + vm = self.get_netbox_vm() + + vcpus = self.get_vcpus() + memory = self.get_memory() + if not vm: + logging.debug('Creating Virtual machine..') + cluster = self.get_netbox_cluster(config.virtual.cluster_name) + + vm = nb.virtualization.virtual_machines.create( + name=hostname, + cluster=cluster.id, + vcpus=vcpus, + memory=memory, + ) + created = True + + self.network = VirtualNetwork(server=self) + self.network.create_or_update_netbox_network_cards() + + if not created and vm.vcpus != vcpus: + vm.vcpus = vcpus + updated += 1 + elif not created and vm.memory != memory: + vm.memory = memory + updated += 1 + + if updated: + vm.save() diff --git a/requirements.txt b/requirements.txt index ab67f3c..176b557 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -pynetbox==4.2.4 +pynetbox==4.3.1 netaddr==0.7.19 netifaces==0.10.9 -pyyaml==5.3 -jsonargparse==2.22.2 +pyyaml==5.3.1 +jsonargparse==2.25.3 diff --git a/setup.py b/setup.py index 7ae5bf5..dfdb021 100644 --- a/setup.py +++ b/setup.py @@ -13,11 +13,11 @@ setup( packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), use_scm_version=True, install_requires=[ - 'pynetbox==4.2.4', + 'pynetbox==4.3.1', 'netaddr==0.7.19', 'netifaces==0.10.9', - 'pyyaml==5.3', - 'jsonargparse==2.22.2', + 'pyyaml==5.3.1', + 'jsonargparse==2.25.3', ], zip_safe=False, keywords=['netbox'],