diff --git a/README.md b/README.md index 7222df2..d571df9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,11 @@ The goal is to generate an existing infrastructure on Netbox and have the abilit * Create physical network interfaces with IPs * Generic ability to guess datacenters through drivers (`cmd` and `file` and custom ones) +# Known limitations + +* The project is only compatible with Linux. +Since it uses `ethtool` and parses `/sys/` directory, it's not compatible with *BSD distributions. + # Configuration ``` diff --git a/netbox_agent/ethtool.py b/netbox_agent/ethtool.py new file mode 100644 index 0000000..91522c9 --- /dev/null +++ b/netbox_agent/ethtool.py @@ -0,0 +1,73 @@ +import re +from shutil import which +import subprocess + +# Originally from https://github.com/opencoff/useful-scripts/blob/master/linktest.py + +# mapping fields from ethtool output to simple names +field_map = { + 'Supported ports': 'ports', + 'Supported link modes': 'sup_link_modes', + 'Supports auto-negotiation': 'sup_autoneg', + 'Advertised link modes': 'adv_link_modes', + 'Advertised auto-negotiation': 'adv_autoneg', + 'Speed': 'speed', + 'Duplex': 'duplex', + 'Port': 'port', + 'Auto-negotiation': 'autoneg', + 'Link detected': 'link', +} + + +class Ethtool(): + """ + This class aims to parse ethtool output + There is several bindings to have something proper, but it requires + compilation and other requirements. + """ + def __init__(self, interface, *args, **kwargs): + self.interface = interface + + def _parse_ethtool_output(self): + """ + parse ethtool output + """ + + output = subprocess.getoutput('ethtool {}'.format(self.interface)) + + fields = {} + field = '' + fields['speed'] = '-' + fields['link'] = '-' + fields['duplex'] = '-' + for line in output.split('\n')[1:]: + line = line.rstrip() + r = line.find(':') + if r > 0: + field = line[:r].strip() + if field not in field_map: + continue + field = field_map[field] + output = line[r+1:].strip() + fields[field] = output + else: + if len(field) > 0 and \ + field in field_map: + fields[field] += ' ' + line.strip() + return fields + + 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 r.groups() > 0: + return {'form_factor': r.groups()[0]} + + def parse(self): + if which('ethtool') is None: + return None + return { + **self._parse_ethtool_output(), + **self._parse_ethtool_module_output(), + } diff --git a/netbox_agent/network.py b/netbox_agent/network.py new file mode 100644 index 0000000..38855ca --- /dev/null +++ b/netbox_agent/network.py @@ -0,0 +1,120 @@ +import os +import re + +from netaddr import IPAddress +import netifaces + +from netbox_agent.config import netbox_instance as nb +from netbox_agent.ethtool import Ethtool + +IFACE_TYPE_100ME_FIXED = 800 +IFACE_TYPE_1GE_FIXED = 1000 +IFACE_TYPE_1GE_GBIC = 1050 +IFACE_TYPE_1GE_SFP = 1100 +IFACE_TYPE_2GE_FIXED = 1120 +IFACE_TYPE_5GE_FIXED = 1130 +IFACE_TYPE_10GE_FIXED = 1150 +IFACE_TYPE_10GE_CX4 = 1170 +IFACE_TYPE_10GE_SFP_PLUS = 1200 +IFACE_TYPE_10GE_XFP = 1300 +IFACE_TYPE_10GE_XENPAK = 1310 +IFACE_TYPE_10GE_X2 = 1320 +IFACE_TYPE_25GE_SFP28 = 1350 +IFACE_TYPE_40GE_QSFP_PLUS = 1400 +IFACE_TYPE_50GE_QSFP28 = 1420 +IFACE_TYPE_100GE_CFP = 1500 +IFACE_TYPE_100GE_CFP2 = 1510 +IFACE_TYPE_100GE_CFP4 = 1520 +IFACE_TYPE_100GE_CPAK = 1550 +IFACE_TYPE_100GE_QSFP28 = 1600 +IFACE_TYPE_200GE_CFP2 = 1650 +IFACE_TYPE_200GE_QSFP56 = 1700 +IFACE_TYPE_400GE_QSFP_DD = 1750 +IFACE_TYPE_OTHER = 32767 + +# Regex to match base interface name +# Doesn't match vlan interfaces and other loopback etc +INTERFACE_REGEX = re.compile('^(eth[0-9]+|ens[0-9]+|enp[0-9]+s[0-9]f[0-9])$') + + +class Network(): + def __init__(self, server, *args, **kwargs): + self.nics = [] + + self.server = server + self.scan() + + def scan(self): + for interface in os.listdir('/sys/class/net/'): + if re.match(INTERFACE_REGEX, interface): + ip_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET) + nic = { + 'name': interface, + 'mac': open('/sys/class/net/{}/address'.format(interface), 'r').read().strip(), + 'ip': [ + '{}/{}'.format( + x['addr'], + IPAddress(x['netmask']).netmask_bits() + ) for x in ip_addr + ] if ip_addr else None, # FIXME: handle IPv6 addresses + 'ethtool': Ethtool(interface).parse() + } + self.nics.append(nic) + + def get_network_cards(self): + return self.nics + + def get_netbox_type_for_nic(self, nic): + if nic.get('ethtool') is None: + return IFACE_TYPE_OTHER + if nic['ethtool']['speed'] == '10000Mb/s': + if nic['ethtool']['port'] == 'FIBRE': + return IFACE_TYPE_10GE_SFP_PLUS + return IFACE_TYPE_10GE_FIXED + elif nic['ethtool']['speed'] == '1000Mb/s': + if nic['ethtool']['port'] == 'FIBRE': + return IFACE_TYPE_1GE_SFP + return IFACE_TYPE_1GE_FIXED + return IFACE_TYPE_OTHER + + def create_netbox_nic(self, device, nic): + # TODO: add Optic Vendor, PN and Serial + return nb.dcim.interfaces.create( + device=device.id, + name=nic['name'], + mac_address=nic['mac'], + type=self.get_netbox_type_for_nic(nic), + ) + + def update_netbox_network_cards(self): + device = self.server.get_netbox_server() + for nic in self.nics: + interface = nb.dcim.interfaces.get( + mac_address=nic['mac'], + ) + # if network doesn't exist we create it + if not interface: + new_interface = self.create_netbox_nic(device, 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']: + netbox_ip = nb.ipam.ip_addresses.get( + address=ip, + ) + if netbox_ip: + netbox_ip.interface = new_interface + netbox_ip.save() + else: + netbox_ip = nb.ipam.ip_addresses.create( + address=ip, + interface=new_interface.id, + status=1, + ) + # or we check if it needs update + else: + # FIXME: implement update + # update name or ip + # see https://github.com/Solvik/netbox_agent/issues/9 + pass diff --git a/netbox_agent/server.py b/netbox_agent/server.py index 1298944..b8b8f07 100644 --- a/netbox_agent/server.py +++ b/netbox_agent/server.py @@ -1,14 +1,10 @@ -import os -import re +from pprint import pprint import socket from netbox_agent.config import netbox_instance as nb from netbox_agent.datacenter import Datacenter import netbox_agent.dmidecode as dmidecode - -# Regex to match base interface name -# Doesn't match vlan interfaces and other loopback etc -INTERFACE_REGEX = re.compile('^(eth[0-9]+|ens[0-9]+|enp[0-9]+s[0-9]f[0-9])$') +from netbox_agent.network import Network class ServerBase(): @@ -20,7 +16,7 @@ class ServerBase(): self.system = self.dmi.get_by_type('System') self.bios = self.dmi.get_by_type('BIOS') - self.network_cards = [] + self.network = Network(server=self) def get_datacenter(self): dc = Datacenter() @@ -65,18 +61,6 @@ class ServerBase(): def get_bios_release_date(self): raise NotImplementedError - def get_network_cards(self): - nics = [] - for interface in os.listdir('/sys/class/net/'): - if re.match(INTERFACE_REGEX, interface): - nic = { - 'name': interface, - 'mac': open('/sys/class/net/{}/address'.format(interface), 'r').read().strip(), - 'ip': None, # FIXME - } - nics.append(nic) - return nics - def _netbox_create_blade_chassis(self, datacenter): device_type = nb.dcim.device_types.get( model=self.get_chassis(), @@ -130,6 +114,9 @@ class ServerBase(): ) return new_server + def get_netbox_server(self): + return nb.dcim.devices.get(serial=self.get_service_tag()) + def netbox_create(self): datacenter = self.get_netbox_datacenter() if self.is_blade(): @@ -159,6 +146,8 @@ class ServerBase(): if not server: self._netbox_create_server() + self.network.update_netbox_network_cards() + def print_debug(self): # FIXME: do something more generic by looping on every get_* methods print('Datacenter:', self.get_datacenter()) @@ -168,3 +157,6 @@ class ServerBase(): 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 diff --git a/requirements.txt b/requirements.txt index 0e602b2..6b76e04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ pynetbox >= 4.0 +netaddr >= 0.7 +netifaces >= 0.10