Compare commits

..

17 commits

Author SHA1 Message Date
4242832396 Merge pull request 'Fix disk size inventory' (#310) from ribetm/disk_size into main
Reviewed-on: #310
2024-10-21 13:35:29 +02:00
13b84b4da1
feat: Run formatters 2024-10-21 12:55:54 +02:00
837860e31a
feat: Add nix tooling 2024-10-21 12:55:40 +02:00
c1a7f661a3 Merge pull request 'Virtualmachine corrections' (#279) from obeone/virtualmachine_corrections into master
Reviewed-on: #279
2024-10-21 12:32:30 +02:00
Grégoire Compagnon
7d268ea0e8
Return 0 if everything ok as excepted in a shell 2024-10-21 12:30:09 +02:00
Grégoire Compagnon
9d496c6854
Check if it's a VM before running lldp related actions 2024-10-21 12:30:09 +02:00
Grégoire Compagnon
6ef23eae4d
Add missing prtint debug 2024-10-21 12:30:09 +02:00
12ceea413c Merge pull request 'Replaced deprecated module pkg_resources' (#311) from ribetm/py312 into master
Reviewed-on: #311
2024-10-21 10:18:18 +02:00
7a968deee9 Merge pull request 'Make lshw scrpping more resilient' (#292) from sinavir/fix_gpu_without_vendor into master
Reviewed-on: #292
2024-10-21 10:15:27 +02:00
d55cbd62fc Merge pull request 'Fix not working tag with VM' (#293) from sinavir/fix_vm_tags into master
Reviewed-on: #293
2024-10-21 10:15:11 +02:00
59ce76fc29 Merge pull request 'feat(network): Batch requests when filtering on interfaces' (#297) from Tom-Hubrecht/master into master
Reviewed-on: #297
2024-10-21 10:01:35 +02:00
Mathis Ribet
c3d3e6857a
Replaced deprecated module pkg_resources
Removed in py3.12
2024-10-17 20:52:28 +02:00
Mathis Ribet
56627c1aa9
Fix disk size 2024-10-17 20:50:23 +02:00
Tom Hubrecht
e04d0c6d59 feat(network): Batch requests when filtering on interfaces
This avoids an issue where the requested URI is too long when many
interfaces are present
2024-05-07 13:54:27 +02:00
sinavir
c9a57de843 fix: vm tags 2024-03-28 12:07:17 +01:00
sinavir
f512e7a0a9 fix: replace list.push by list.append 2024-03-27 18:49:05 +01:00
sinavir
116334be2f fix: make lshw props finding more resilient 2024-03-27 17:12:48 +01:00
38 changed files with 1700 additions and 1288 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

2
.gitignore vendored
View file

@ -182,3 +182,5 @@ dmypy.json
netbox-docker netbox-docker
/.vscode /.vscode
.direnv
.pre-commit-config.yaml

55
default.nix Normal file
View file

@ -0,0 +1,55 @@
{
sources ? import ./npins,
pkgs ? import sources.nixpkgs { },
}:
let
checks = (import sources.git-hooks).run {
src = ./.;
hooks =
{
commitizen.enable = true;
}
// (pkgs.lib.genAttrs
[
"black"
"isort"
"ruff"
]
(hook: {
enable = true;
stages = [ "pre-push" ];
})
);
};
python3 = pkgs.python3.override {
packageOverrides = self: _: {
netifaces2 = self.callPackage ./nix/netifaces2.nix { };
};
};
in
{
devShell = pkgs.mkShell {
name = "netbox-agent.dev";
packages = [
(python3.withPackages (ps: [
ps.pynetbox
ps.netaddr
ps.netifaces2
ps.pyyaml
ps.jsonargparse
ps.python-slugify
ps.packaging
ps.distro
]))
] ++ checks.enabledPackages;
shellHook = ''
${checks.shellHook}
'';
};
}

View file

@ -1,6 +1,7 @@
from pkg_resources import DistributionNotFound, get_distribution from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _get_version
try: try:
__version__ = get_distribution(__name__).version __version__ = _get_version(__name__)
except DistributionNotFound: except PackageNotFoundError:
pass pass

View file

@ -1,4 +1,5 @@
from packaging import version from packaging import version
import netbox_agent.dmidecode as dmidecode import netbox_agent.dmidecode as dmidecode
from netbox_agent.config import config from netbox_agent.config import config
from netbox_agent.config import netbox_instance as nb from netbox_agent.config import netbox_instance as nb
@ -11,12 +12,12 @@ from netbox_agent.vendors.supermicro import SupermicroHost
from netbox_agent.virtualmachine import VirtualMachine, is_vm from netbox_agent.virtualmachine import VirtualMachine, is_vm
MANUFACTURERS = { MANUFACTURERS = {
'Dell Inc.': DellHost, "Dell Inc.": DellHost,
'HP': HPHost, "HP": HPHost,
'HPE': HPHost, "HPE": HPHost,
'Supermicro': SupermicroHost, "Supermicro": SupermicroHost,
'Quanta Cloud Technology Inc.': QCTHost, "Quanta Cloud Technology Inc.": QCTHost,
'Generic': GenericHost, "Generic": GenericHost,
} }
@ -25,21 +26,29 @@ def run(config):
if config.virtual.enabled or is_vm(dmi): if config.virtual.enabled or is_vm(dmi):
if not config.virtual.cluster_name: if not config.virtual.cluster_name:
raise Exception('virtual.cluster_name parameter is mandatory because it\'s a VM') raise Exception(
"virtual.cluster_name parameter is mandatory because it's a VM"
)
server = VirtualMachine(dmi=dmi) server = VirtualMachine(dmi=dmi)
else: else:
manufacturer = dmidecode.get_by_type(dmi, 'Chassis')[0].get('Manufacturer') manufacturer = dmidecode.get_by_type(dmi, "Chassis")[0].get("Manufacturer")
try: try:
server = MANUFACTURERS[manufacturer](dmi=dmi) server = MANUFACTURERS[manufacturer](dmi=dmi)
except KeyError: except KeyError:
server = GenericHost(dmi=dmi) server = GenericHost(dmi=dmi)
if version.parse(nb.version) < version.parse('3.7'): if version.parse(nb.version) < version.parse("3.7"):
print('netbox-agent is not compatible with Netbox prior to version 3.7') print("netbox-agent is not compatible with Netbox prior to version 3.7")
return False return False
if config.register or config.update_all or config.update_network or \ if (
config.update_location or config.update_inventory or config.update_psu: 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) server.netbox_create_or_update(config)
if config.debug: if config.debug:
server.print_debug() server.print_debug()
@ -47,8 +56,8 @@ def run(config):
def main(): def main():
return run(config) return 0 if run(config) else 1
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View file

@ -10,85 +10,148 @@ import urllib3
def get_config(): def get_config():
p = jsonargparse.ArgumentParser( p = jsonargparse.ArgumentParser(
default_config_files=[ default_config_files=[
'/etc/netbox_agent.yaml', "/etc/netbox_agent.yaml",
'~/.config/netbox_agent.yaml', "~/.config/netbox_agent.yaml",
'~/.netbox_agent.yaml', "~/.netbox_agent.yaml",
], ],
prog='netbox_agent', prog="netbox_agent",
description="Netbox agent to run on your infrastructure's servers", description="Netbox agent to run on your infrastructure's servers",
env_prefix='NETBOX_AGENT_', env_prefix="NETBOX_AGENT_",
default_env=True default_env=True,
) )
p.add_argument('-c', '--config', action=jsonargparse.ActionConfigFile) p.add_argument("-c", "--config", action=jsonargparse.ActionConfigFile)
p.add_argument('-r', '--register', action='store_true', help='Register server to Netbox') p.add_argument(
p.add_argument('-u', '--update-all', action='store_true', help='Update all infos in Netbox') "-r", "--register", action="store_true", help="Register server to Netbox"
p.add_argument('-d', '--debug', action='store_true', help='Print debug infos') )
p.add_argument('--update-network', action='store_true', help='Update network') p.add_argument(
p.add_argument('--update-inventory', action='store_true', help='Update inventory') "-u", "--update-all", action="store_true", help="Update all infos in Netbox"
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("-d", "--debug", action="store_true", help="Print debug infos")
p.add_argument('--purge-old-devices', action='store_true', p.add_argument("--update-network", action="store_true", help="Update network")
help='Purge existing (old ?) devices having same name but different serial') p.add_argument("--update-inventory", action="store_true", help="Update inventory")
p.add_argument('--expansion-as-device', action='store_true', p.add_argument("--update-location", action="store_true", help="Update location")
help='Manage blade expansions as external devices') 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("--log_level", default="debug")
p.add_argument('--netbox.ssl_ca_certs_file', help='SSL CA certificates file') 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.url", help="Netbox URL")
p.add_argument('--netbox.token', help='Netbox API Token') p.add_argument("--netbox.token", help="Netbox API Token")
p.add_argument('--netbox.ssl_verify', default=True, action='store_true', p.add_argument(
help='Disable SSL verification') "--netbox.ssl_verify",
p.add_argument('--virtual.enabled', action='store_true', help='Is a virtual machine or not') default=True,
p.add_argument('--virtual.cluster_name', help='Cluster name of VM') action="store_true",
p.add_argument('--hostname_cmd', default=None, help="Disable SSL verification",
help="Command to output hostname, used as Device's name in netbox") )
p.add_argument('--device.platform', default=None, p.add_argument(
help='Override device platform. Here we use OS distribution.') "--virtual.enabled", action="store_true", help="Is a virtual machine or not"
p.add_argument('--device.tags', default=r'', )
help='tags to use for a host') p.add_argument("--virtual.cluster_name", help="Cluster name of VM")
p.add_argument('--preserve-tags', action='store_true', help='Append new unique tags, preserve those already present') p.add_argument(
p.add_argument('--device.custom_fields', default=r'', "--hostname_cmd",
help='custom_fields to use for a host, eg: field1=v1,field2=v2') default=None,
p.add_argument('--device.blade_role', default=r'Blade', help="Command to output hostname, used as Device's name in netbox",
help='role to use for a blade server') )
p.add_argument('--device.chassis_role', default=r'Server Chassis', p.add_argument(
help='role to use for a chassis') "--device.platform",
p.add_argument('--device.server_role', default=r'Server', default=None,
help='role to use for a server') help="Override device platform. Here we use OS distribution.",
p.add_argument('--tenant.driver', )
help='tenant driver, ie cmd, file') p.add_argument("--device.tags", default=r"", help="tags to use for a host")
p.add_argument('--tenant.driver_file', p.add_argument(
help='tenant driver custom driver file path') "--preserve-tags",
p.add_argument('--tenant.regex', action="store_true",
help='tenant regex to extract Netbox tenant slug') help="Append new unique tags, preserve those already present",
p.add_argument('--datacenter_location.driver', )
help='Datacenter location driver, ie: cmd, file') p.add_argument(
p.add_argument('--datacenter_location.driver_file', "--device.custom_fields",
help='Datacenter location custom driver file path') default=r"",
p.add_argument('--datacenter_location.regex', help="custom_fields to use for a host, eg: field1=v1,field2=v2",
help='Datacenter location regex to extract Netbox DC slug') )
p.add_argument('--rack_location.driver', help='Rack location driver, ie: cmd, file') p.add_argument(
p.add_argument('--rack_location.driver_file', help='Rack location custom driver file path') "--device.blade_role", default=r"Blade", help="role to use for a blade server"
p.add_argument('--rack_location.regex', help='Rack location regex to extract Rack name') )
p.add_argument('--slot_location.driver', help='Slot location driver, ie: cmd, file') p.add_argument(
p.add_argument('--slot_location.driver_file', help='Slot location custom driver file path') "--device.chassis_role",
p.add_argument('--slot_location.regex', help='Slot location regex to extract slot name') default=r"Server Chassis",
p.add_argument('--network.ignore_interfaces', default=r'(dummy.*|docker.*)', help="role to use for a chassis",
help='Regex to ignore interfaces') )
p.add_argument('--network.ignore_ips', default=r'^(127\.0\.0\..*|fe80.*|::1.*)', p.add_argument(
help='Regex to ignore IPs') "--device.server_role", default=r"Server", help="role to use for a server"
p.add_argument('--network.ipmi', default=True, help='Enable gathering IPMI information') )
p.add_argument('--network.lldp', help='Enable auto-cabling feature through LLDP infos') p.add_argument("--tenant.driver", help="tenant driver, ie cmd, file")
p.add_argument('--inventory', action='store_true', p.add_argument("--tenant.driver_file", help="tenant driver custom driver file path")
help='Enable HW inventory (CPU, Memory, RAID Cards, Disks) feature') p.add_argument("--tenant.regex", help="tenant regex to extract Netbox tenant slug")
p.add_argument('--process-virtual-drives', action='store_true', p.add_argument(
help='Process virtual drives information from RAID ' "--datacenter_location.driver", help="Datacenter location driver, ie: cmd, file"
'controllers to fill disk custom_fields') )
p.add_argument('--force-disk-refresh', action='store_true', p.add_argument(
help='Forces disks detection reprocessing') "--datacenter_location.driver_file",
p.add_argument('--dump-disks-map', help="Datacenter location custom driver file path",
help='File path to dump physical/virtual disks map') )
p.add_argument(
"--datacenter_location.regex",
help="Datacenter location regex to extract Netbox DC slug",
)
p.add_argument("--rack_location.driver", help="Rack location driver, ie: cmd, file")
p.add_argument(
"--rack_location.driver_file", help="Rack location custom driver file path"
)
p.add_argument(
"--rack_location.regex", help="Rack location regex to extract Rack name"
)
p.add_argument("--slot_location.driver", help="Slot location driver, ie: cmd, file")
p.add_argument(
"--slot_location.driver_file", help="Slot location custom driver file path"
)
p.add_argument(
"--slot_location.regex", help="Slot location regex to extract slot name"
)
p.add_argument(
"--network.ignore_interfaces",
default=r"(dummy.*|docker.*)",
help="Regex to ignore interfaces",
)
p.add_argument(
"--network.ignore_ips",
default=r"^(127\.0\.0\..*|fe80.*|::1.*)",
help="Regex to ignore IPs",
)
p.add_argument(
"--network.ipmi", default=True, help="Enable gathering IPMI information"
)
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() options = p.parse_args()
return options return options
@ -99,7 +162,7 @@ config = get_config()
def get_netbox_instance(): def get_netbox_instance():
if config.netbox.url is None or config.netbox.token is None: if config.netbox.url is None or config.netbox.token is None:
logging.error('Netbox URL and token are mandatory') logging.error("Netbox URL and token are mandatory")
sys.exit(1) sys.exit(1)
nb = pynetbox.api( nb = pynetbox.api(

View file

@ -5,55 +5,57 @@ import sys
from netbox_agent.misc import is_tool from netbox_agent.misc import is_tool
_handle_re = _re.compile('^Handle\\s+(.+),\\s+DMI\\s+type\\s+(\\d+),\\s+(\\d+)\\s+bytes$') _handle_re = _re.compile(
_in_block_re = _re.compile('^\\t\\t(.+)$') "^Handle\\s+(.+),\\s+DMI\\s+type\\s+(\\d+),\\s+(\\d+)\\s+bytes$"
_record_re = _re.compile('\\t(.+):\\s+(.+)$') )
_record2_re = _re.compile('\\t(.+):$') _in_block_re = _re.compile("^\\t\\t(.+)$")
_record_re = _re.compile("\\t(.+):\\s+(.+)$")
_record2_re = _re.compile("\\t(.+):$")
_type2str = { _type2str = {
0: 'BIOS', 0: "BIOS",
1: 'System', 1: "System",
2: 'Baseboard', 2: "Baseboard",
3: 'Chassis', 3: "Chassis",
4: 'Processor', 4: "Processor",
5: 'Memory Controller', 5: "Memory Controller",
6: 'Memory Module', 6: "Memory Module",
7: 'Cache', 7: "Cache",
8: 'Port Connector', 8: "Port Connector",
9: 'System Slots', 9: "System Slots",
10: ' On Board Devices', 10: " On Board Devices",
11: ' OEM Strings', 11: " OEM Strings",
12: ' System Configuration Options', 12: " System Configuration Options",
13: ' BIOS Language', 13: " BIOS Language",
14: ' Group Associations', 14: " Group Associations",
15: ' System Event Log', 15: " System Event Log",
16: ' Physical Memory Array', 16: " Physical Memory Array",
17: ' Memory Device', 17: " Memory Device",
18: ' 32-bit Memory Error', 18: " 32-bit Memory Error",
19: ' Memory Array Mapped Address', 19: " Memory Array Mapped Address",
20: ' Memory Device Mapped Address', 20: " Memory Device Mapped Address",
21: ' Built-in Pointing Device', 21: " Built-in Pointing Device",
22: ' Portable Battery', 22: " Portable Battery",
23: ' System Reset', 23: " System Reset",
24: ' Hardware Security', 24: " Hardware Security",
25: ' System Power Controls', 25: " System Power Controls",
26: ' Voltage Probe', 26: " Voltage Probe",
27: ' Cooling Device', 27: " Cooling Device",
28: ' Temperature Probe', 28: " Temperature Probe",
29: ' Electrical Current Probe', 29: " Electrical Current Probe",
30: ' Out-of-band Remote Access', 30: " Out-of-band Remote Access",
31: ' Boot Integrity Services', 31: " Boot Integrity Services",
32: ' System Boot', 32: " System Boot",
33: ' 64-bit Memory Error', 33: " 64-bit Memory Error",
34: ' Management Device', 34: " Management Device",
35: ' Management Device Component', 35: " Management Device Component",
36: ' Management Device Threshold Data', 36: " Management Device Threshold Data",
37: ' Memory Channel', 37: " Memory Channel",
38: ' IPMI Device', 38: " IPMI Device",
39: ' Power Supply', 39: " Power Supply",
40: ' Additional Information', 40: " Additional Information",
41: ' Onboard Devices Extended Information', 41: " Onboard Devices Extended Information",
42: ' Management Controller Host Interface' 42: " Management Controller Host Interface",
} }
_str2type = {} _str2type = {}
for type_id, type_str in _type2str.items(): for type_id, type_str in _type2str.items():
@ -70,7 +72,7 @@ def parse(output=None):
else: else:
buffer = _execute_cmd() buffer = _execute_cmd()
if isinstance(buffer, bytes): if isinstance(buffer, bytes):
buffer = buffer.decode('utf-8') buffer = buffer.decode("utf-8")
_data = _parse(buffer) _data = _parse(buffer)
return _data return _data
@ -129,24 +131,31 @@ def get_by_type(data, type_id):
result = [] result = []
for entry in data.values(): for entry in data.values():
if entry['DMIType'] == type_id: if entry["DMIType"] == type_id:
result.append(entry) result.append(entry)
return result return result
def _execute_cmd(): def _execute_cmd():
if not is_tool('dmidecode'): if not is_tool("dmidecode"):
logging.error('Dmidecode does not seem to be present on your system. Add it your path or ' logging.error(
'check the compatibility of this project with your distro.') "Dmidecode does not seem to be present on your system. Add it your path or "
"check the compatibility of this project with your distro."
)
sys.exit(1) sys.exit(1)
return _subprocess.check_output(['dmidecode', ], stderr=_subprocess.PIPE) return _subprocess.check_output(
[
"dmidecode",
],
stderr=_subprocess.PIPE,
)
def _parse(buffer): def _parse(buffer):
output_data = {} output_data = {}
# Each record is separated by double newlines # Each record is separated by double newlines
split_output = buffer.split('\n\n') split_output = buffer.split("\n\n")
for record in split_output: for record in split_output:
record_element = record.splitlines() record_element = record.splitlines()
@ -164,21 +173,21 @@ def _parse(buffer):
dmi_handle = handle_data[0] dmi_handle = handle_data[0]
output_data[dmi_handle] = {} output_data[dmi_handle] = {}
output_data[dmi_handle]['DMIType'] = int(handle_data[1]) output_data[dmi_handle]["DMIType"] = int(handle_data[1])
output_data[dmi_handle]['DMISize'] = int(handle_data[2]) output_data[dmi_handle]["DMISize"] = int(handle_data[2])
# Okay, we know 2nd line == name # Okay, we know 2nd line == name
output_data[dmi_handle]['DMIName'] = record_element[1] output_data[dmi_handle]["DMIName"] = record_element[1]
in_block_elemet = '' in_block_elemet = ""
in_block_list = '' in_block_list = ""
# Loop over the rest of the record, gathering values # Loop over the rest of the record, gathering values
for i in range(2, len(record_element), 1): for i in range(2, len(record_element), 1):
if i >= len(record_element): if i >= len(record_element):
break break
# Check whether we are inside a \t\t block # Check whether we are inside a \t\t block
if in_block_elemet != '': if in_block_elemet != "":
in_block_data = _in_block_re.findall(record_element[i]) in_block_data = _in_block_re.findall(record_element[i])
if in_block_data: if in_block_data:
@ -192,7 +201,7 @@ def _parse(buffer):
else: else:
# We are out of the \t\t block; reset it again, and let # We are out of the \t\t block; reset it again, and let
# the parsing continue # the parsing continue
in_block_elemet = '' in_block_elemet = ""
record_data = _record_re.findall(record_element[i]) record_data = _record_re.findall(record_element[i])
@ -208,7 +217,7 @@ def _parse(buffer):
# This is an array of data - let the loop know we are inside # This is an array of data - let the loop know we are inside
# an array block # an array block
in_block_elemet = record_data2[0] in_block_elemet = record_data2[0]
in_block_list = '' in_block_list = ""
continue continue

View file

@ -2,7 +2,7 @@ import re
def get(value, regex): def get(value, regex):
for line in open(value, 'r'): for line in open(value, "r"):
r = re.search(regex, line) r = re.search(regex, line)
if r and len(r.groups()) > 0: if r and len(r.groups()) > 0:
return r.groups()[0] return r.groups()[0]

View file

@ -6,16 +6,16 @@ from shutil import which
# mapping fields from ethtool output to simple names # mapping fields from ethtool output to simple names
field_map = { field_map = {
'Supported ports': 'ports', "Supported ports": "ports",
'Supported link modes': 'sup_link_modes', "Supported link modes": "sup_link_modes",
'Supports auto-negotiation': 'sup_autoneg', "Supports auto-negotiation": "sup_autoneg",
'Advertised link modes': 'adv_link_modes', "Advertised link modes": "adv_link_modes",
'Advertised auto-negotiation': 'adv_autoneg', "Advertised auto-negotiation": "adv_autoneg",
'Speed': 'speed', "Speed": "speed",
'Duplex': 'duplex', "Duplex": "duplex",
'Port': 'port', "Port": "port",
'Auto-negotiation': 'autoneg', "Auto-negotiation": "autoneg",
'Link detected': 'link', "Link detected": "link",
} }
@ -25,7 +25,7 @@ def merge_two_dicts(x, y):
return z return z
class Ethtool(): class Ethtool:
""" """
This class aims to parse ethtool output This class aims to parse ethtool output
There is several bindings to have something proper, but it requires There is several bindings to have something proper, but it requires
@ -40,16 +40,16 @@ class Ethtool():
parse ethtool output parse ethtool output
""" """
output = subprocess.getoutput('ethtool {}'.format(self.interface)) output = subprocess.getoutput("ethtool {}".format(self.interface))
fields = {} fields = {}
field = '' field = ""
fields['speed'] = '-' fields["speed"] = "-"
fields['link'] = '-' fields["link"] = "-"
fields['duplex'] = '-' fields["duplex"] = "-"
for line in output.split('\n')[1:]: for line in output.split("\n")[1:]:
line = line.rstrip() line = line.rstrip()
r = line.find(':') r = line.find(":")
if r > 0: if r > 0:
field = line[:r].strip() field = line[:r].strip()
if field not in field_map: if field not in field_map:
@ -58,21 +58,22 @@ class Ethtool():
output = line[r + 1 :].strip() output = line[r + 1 :].strip()
fields[field] = output fields[field] = output
else: else:
if len(field) > 0 and \ if len(field) > 0 and field in field_map:
field in field_map: fields[field] += " " + line.strip()
fields[field] += ' ' + line.strip()
return fields return fields
def _parse_ethtool_module_output(self): def _parse_ethtool_module_output(self):
status, output = subprocess.getstatusoutput('ethtool -m {}'.format(self.interface)) status, output = subprocess.getstatusoutput(
"ethtool -m {}".format(self.interface)
)
if status == 0: if status == 0:
r = re.search(r'Identifier.*\((\w+)\)', output) r = re.search(r"Identifier.*\((\w+)\)", output)
if r and len(r.groups()) > 0: if r and len(r.groups()) > 0:
return {'form_factor': r.groups()[0]} return {"form_factor": r.groups()[0]}
return {} return {}
def parse(self): def parse(self):
if which('ethtool') is None: if which("ethtool") is None:
return None return None
output = self._parse_ethtool_output() output = self._parse_ethtool_output()
output.update(self._parse_ethtool_module_output()) output.update(self._parse_ethtool_module_output())

View file

@ -1,3 +1,11 @@
import json
import logging
import re
import sys
import traceback
import pynetbox
from netbox_agent.config import config from netbox_agent.config import config
from netbox_agent.config import netbox_instance as nb from netbox_agent.config import netbox_instance as nb
from netbox_agent.lshw import LSHW from netbox_agent.lshw import LSHW
@ -5,25 +13,19 @@ from netbox_agent.misc import get_vendor, is_tool
from netbox_agent.raid.hp import HPRaid from netbox_agent.raid.hp import HPRaid
from netbox_agent.raid.omreport import OmreportRaid from netbox_agent.raid.omreport import OmreportRaid
from netbox_agent.raid.storcli import StorcliRaid from netbox_agent.raid.storcli import StorcliRaid
import traceback
import pynetbox
import logging
import json
import re
INVENTORY_TAG = { INVENTORY_TAG = {
'cpu': {'name': 'hw:cpu', 'slug': 'hw-cpu'}, "cpu": {"name": "hw:cpu", "slug": "hw-cpu"},
'gpu': {'name': 'hw:gpu', 'slug': 'hw-gpu'}, "gpu": {"name": "hw:gpu", "slug": "hw-gpu"},
'disk': {'name': 'hw:disk', 'slug': 'hw-disk'}, "disk": {"name": "hw:disk", "slug": "hw-disk"},
'interface': {'name': 'hw:interface', 'slug': 'hw-interface'}, "interface": {"name": "hw:interface", "slug": "hw-interface"},
'memory': {'name': 'hw:memory', 'slug': 'hw-memory'}, "memory": {"name": "hw:memory", "slug": "hw-memory"},
'motherboard': {'name': 'hw:motherboard', 'slug': 'hw-motherboard'}, "motherboard": {"name": "hw:motherboard", "slug": "hw-motherboard"},
'raid_card': {'name': 'hw:raid_card', 'slug': 'hw-raid-card'}, "raid_card": {"name": "hw:raid_card", "slug": "hw-raid-card"},
} }
class Inventory(): class Inventory:
""" """
Better Inventory items coming, see: Better Inventory items coming, see:
- https://github.com/netbox-community/netbox/issues/3087 - https://github.com/netbox-community/netbox/issues/3087
@ -62,14 +64,12 @@ class Inventory():
def create_netbox_tags(self): def create_netbox_tags(self):
ret = [] ret = []
for key, tag in INVENTORY_TAG.items(): for key, tag in INVENTORY_TAG.items():
nb_tag = nb.extras.tags.get( nb_tag = nb.extras.tags.get(name=tag["name"])
name=tag['name']
)
if not nb_tag: if not nb_tag:
nb_tag = nb.extras.tags.create( nb_tag = nb.extras.tags.create(
name=tag['name'], name=tag["name"],
slug=tag['slug'], slug=tag["slug"],
comments=tag['name'], comments=tag["name"],
) )
ret.append(nb_tag) ret.append(nb_tag)
return ret return ret
@ -82,29 +82,28 @@ class Inventory():
name=name, name=name,
) )
if not manufacturer: if not manufacturer:
logging.info('Creating missing manufacturer {name}'.format(name=name)) logging.info("Creating missing manufacturer {name}".format(name=name))
manufacturer = nb.dcim.manufacturers.create( manufacturer = nb.dcim.manufacturers.create(
name=name, name=name,
slug=re.sub('[^A-Za-z0-9]+', '-', name).lower(), slug=re.sub("[^A-Za-z0-9]+", "-", name).lower(),
) )
logging.info('Creating missing manufacturer {name}'.format(name=name)) logging.info("Creating missing manufacturer {name}".format(name=name))
return manufacturer return manufacturer
def get_netbox_inventory(self, device_id, tag): def get_netbox_inventory(self, device_id, tag):
try: try:
items = nb.dcim.inventory_items.filter( items = nb.dcim.inventory_items.filter(device_id=device_id, tag=tag)
device_id=device_id,
tag=tag
)
except pynetbox.core.query.RequestError: except pynetbox.core.query.RequestError:
logging.info('Tag {tag} is missing, returning empty array.'.format(tag=tag)) logging.info("Tag {tag} is missing, returning empty array.".format(tag=tag))
items = [] items = []
return list(items) return list(items)
def create_netbox_inventory_item(self, device_id, tags, vendor, name, serial, description): def create_netbox_inventory_item(
self, device_id, tags, vendor, name, serial, description
):
manufacturer = self.find_or_create_manufacturer(vendor) manufacturer = self.find_or_create_manufacturer(vendor)
_ = nb.dcim.inventory_items.create( _ = nb.dcim.inventory_items.create(
@ -112,26 +111,25 @@ class Inventory():
manufacturer=manufacturer.id, manufacturer=manufacturer.id,
discovered=True, discovered=True,
tags=tags, tags=tags,
name='{}'.format(name), name="{}".format(name),
serial='{}'.format(serial), serial="{}".format(serial),
description=description description=description,
) )
logging.info('Creating inventory item {} {}/{} {} '.format( logging.info(
vendor, "Creating inventory item {} {}/{} {} ".format(
name, vendor, name, serial, description
serial, )
description)
) )
def get_hw_motherboards(self): def get_hw_motherboards(self):
motherboards = [] motherboards = []
m = {} m = {}
m['serial'] = self.lshw.motherboard_serial m["serial"] = self.lshw.motherboard_serial
m['vendor'] = self.lshw.vendor m["vendor"] = self.lshw.vendor
m['name'] = '{} {}'.format(self.lshw.vendor, self.lshw.motherboard) m["name"] = "{} {}".format(self.lshw.vendor, self.lshw.motherboard)
m['description'] = '{} Motherboard'.format(self.lshw.motherboard) m["description"] = "{} Motherboard".format(self.lshw.motherboard)
motherboards.append(m) motherboards.append(m)
@ -141,27 +139,29 @@ class Inventory():
motherboards = self.get_hw_motherboards() motherboards = self.get_hw_motherboards()
nb_motherboards = self.get_netbox_inventory( nb_motherboards = self.get_netbox_inventory(
device_id=self.device_id, device_id=self.device_id, tag=INVENTORY_TAG["motherboard"]["slug"]
tag=INVENTORY_TAG['motherboard']['slug']) )
for nb_motherboard in nb_motherboards: for nb_motherboard in nb_motherboards:
if nb_motherboard.serial not in [x['serial'] for x in motherboards]: if nb_motherboard.serial not in [x["serial"] for x in motherboards]:
logging.info('Deleting unknown motherboard {motherboard}/{serial}'.format( logging.info(
"Deleting unknown motherboard {motherboard}/{serial}".format(
motherboard=self.lshw.motherboard, motherboard=self.lshw.motherboard,
serial=nb_motherboard.serial, serial=nb_motherboard.serial,
)) )
)
nb_motherboard.delete() nb_motherboard.delete()
# create interfaces that are not in netbox # create interfaces that are not in netbox
for motherboard in motherboards: for motherboard in motherboards:
if motherboard.get('serial') not in [x.serial for x in nb_motherboards]: if motherboard.get("serial") not in [x.serial for x in nb_motherboards]:
self.create_netbox_inventory_item( self.create_netbox_inventory_item(
device_id=self.device_id, device_id=self.device_id,
tags=[{'name': INVENTORY_TAG['motherboard']['name']}], tags=[{"name": INVENTORY_TAG["motherboard"]["name"]}],
vendor='{}'.format(motherboard.get('vendor', 'N/A')), vendor="{}".format(motherboard.get("vendor", "N/A")),
serial='{}'.format(motherboard.get('serial', 'No SN')), serial="{}".format(motherboard.get("serial", "No SN")),
name='{}'.format(motherboard.get('name')), name="{}".format(motherboard.get("name")),
description='{}'.format(motherboard.get('description')) description="{}".format(motherboard.get("description")),
) )
def create_netbox_interface(self, iface): def create_netbox_interface(self, iface):
@ -170,56 +170,57 @@ class Inventory():
device=self.device_id, device=self.device_id,
manufacturer=manufacturer.id, manufacturer=manufacturer.id,
discovered=True, discovered=True,
tags=[{'name': INVENTORY_TAG['interface']['name']}], tags=[{"name": INVENTORY_TAG["interface"]["name"]}],
name="{}".format(iface['product']), name="{}".format(iface["product"]),
serial='{}'.format(iface['serial']), serial="{}".format(iface["serial"]),
description='{} {}'.format(iface['description'], iface['name']) description="{} {}".format(iface["description"], iface["name"]),
) )
def do_netbox_interfaces(self): def do_netbox_interfaces(self):
nb_interfaces = self.get_netbox_inventory( nb_interfaces = self.get_netbox_inventory(
device_id=self.device_id, device_id=self.device_id, tag=INVENTORY_TAG["interface"]["slug"]
tag=INVENTORY_TAG['interface']['slug']) )
interfaces = self.lshw.interfaces interfaces = self.lshw.interfaces
# delete interfaces that are in netbox but not locally # delete interfaces that are in netbox but not locally
# use the serial_number has the comparison element # use the serial_number has the comparison element
for nb_interface in nb_interfaces: for nb_interface in nb_interfaces:
if nb_interface.serial not in [x['serial'] for x in interfaces]: if nb_interface.serial not in [x["serial"] for x in interfaces]:
logging.info('Deleting unknown interface {serial}'.format( logging.info(
"Deleting unknown interface {serial}".format(
serial=nb_interface.serial, serial=nb_interface.serial,
)) )
)
nb_interface.delete() nb_interface.delete()
# create interfaces that are not in netbox # create interfaces that are not in netbox
for iface in interfaces: for iface in interfaces:
if iface.get('serial') not in [x.serial for x in nb_interfaces]: if iface.get("serial") not in [x.serial for x in nb_interfaces]:
self.create_netbox_interface(iface) self.create_netbox_interface(iface)
def create_netbox_cpus(self): def create_netbox_cpus(self):
for cpu in self.lshw.get_hw_linux('cpu'): for cpu in self.lshw.get_hw_linux("cpu"):
manufacturer = self.find_or_create_manufacturer(cpu["vendor"]) manufacturer = self.find_or_create_manufacturer(cpu["vendor"])
_ = nb.dcim.inventory_items.create( _ = nb.dcim.inventory_items.create(
device=self.device_id, device=self.device_id,
manufacturer=manufacturer.id, manufacturer=manufacturer.id,
discovered=True, discovered=True,
tags=[{'name': INVENTORY_TAG['cpu']['name']}], tags=[{"name": INVENTORY_TAG["cpu"]["name"]}],
name=cpu['product'], name=cpu["product"],
description='CPU {}'.format(cpu['location']), description="CPU {}".format(cpu["location"]),
# asset_tag=cpu['location'] # asset_tag=cpu['location']
) )
logging.info('Creating CPU model {}'.format(cpu['product'])) logging.info("Creating CPU model {}".format(cpu["product"]))
def do_netbox_cpus(self): def do_netbox_cpus(self):
cpus = self.lshw.get_hw_linux('cpu') cpus = self.lshw.get_hw_linux("cpu")
nb_cpus = self.get_netbox_inventory( nb_cpus = self.get_netbox_inventory(
device_id=self.device_id, device_id=self.device_id,
tag=INVENTORY_TAG['cpu']['slug'], tag=INVENTORY_TAG["cpu"]["slug"],
) )
if not len(nb_cpus) or \ if not len(nb_cpus) or len(nb_cpus) and len(cpus) != len(nb_cpus):
len(nb_cpus) and len(cpus) != len(nb_cpus):
for x in nb_cpus: for x in nb_cpus:
x.delete() x.delete()
@ -227,13 +228,13 @@ class Inventory():
def get_raid_cards(self, filter_cards=False): def get_raid_cards(self, filter_cards=False):
raid_class = None raid_class = None
if self.server.manufacturer in ('Dell', 'Huawei'): if self.server.manufacturer in ("Dell", "Huawei"):
if is_tool('omreport'): if is_tool("omreport"):
raid_class = OmreportRaid raid_class = OmreportRaid
if is_tool('storcli'): if is_tool("storcli"):
raid_class = StorcliRaid raid_class = StorcliRaid
elif self.server.manufacturer in ('HP', 'HPE'): elif self.server.manufacturer in ("HP", "HPE"):
if is_tool('ssacli'): if is_tool("ssacli"):
raid_class = HPRaid raid_class = HPRaid
if not raid_class: if not raid_class:
@ -241,19 +242,21 @@ class Inventory():
self.raid = raid_class() self.raid = raid_class()
if filter_cards and config.expansion_as_device \ if (
and self.server.own_expansion_slot(): filter_cards
and config.expansion_as_device
and self.server.own_expansion_slot()
):
return [ return [
c for c in self.raid.get_controllers() c
for c in self.raid.get_controllers()
if c.is_external() is self.update_expansion if c.is_external() is self.update_expansion
] ]
else: else:
return self.raid.get_controllers() return self.raid.get_controllers()
def create_netbox_raid_card(self, raid_card): def create_netbox_raid_card(self, raid_card):
manufacturer = self.find_or_create_manufacturer( manufacturer = self.find_or_create_manufacturer(raid_card.get_manufacturer())
raid_card.get_manufacturer()
)
name = raid_card.get_product_name() name = raid_card.get_product_name()
serial = raid_card.get_serial_number() serial = raid_card.get_serial_number()
@ -261,15 +264,17 @@ class Inventory():
device=self.device_id, device=self.device_id,
discovered=True, discovered=True,
manufacturer=manufacturer.id if manufacturer else None, manufacturer=manufacturer.id if manufacturer else None,
tags=[{'name': INVENTORY_TAG['raid_card']['name']}], tags=[{"name": INVENTORY_TAG["raid_card"]["name"]}],
name='{}'.format(name), name="{}".format(name),
serial='{}'.format(serial), serial="{}".format(serial),
description='RAID Card', description="RAID Card",
) )
logging.info('Creating RAID Card {name} (SN: {serial})'.format( logging.info(
"Creating RAID Card {name} (SN: {serial})".format(
name=name, name=name,
serial=serial, serial=serial,
)) )
)
return nb_raid_card return nb_raid_card
def do_netbox_raid_cards(self): def do_netbox_raid_cards(self):
@ -284,8 +289,7 @@ class Inventory():
""" """
nb_raid_cards = self.get_netbox_inventory( nb_raid_cards = self.get_netbox_inventory(
device_id=self.device_id, device_id=self.device_id, tag=[INVENTORY_TAG["raid_card"]["slug"]]
tag=[INVENTORY_TAG['raid_card']['slug']]
) )
raid_cards = self.get_raid_cards(filter_cards=True) raid_cards = self.get_raid_cards(filter_cards=True)
@ -293,9 +297,11 @@ class Inventory():
# use the serial_number has the comparison element # use the serial_number has the comparison element
for nb_raid_card in nb_raid_cards: for nb_raid_card in nb_raid_cards:
if nb_raid_card.serial not in [x.get_serial_number() for x in raid_cards]: if nb_raid_card.serial not in [x.get_serial_number() for x in raid_cards]:
logging.info('Deleting unknown locally RAID Card {serial}'.format( logging.info(
"Deleting unknown locally RAID Card {serial}".format(
serial=nb_raid_card.serial, serial=nb_raid_card.serial,
)) )
)
nb_raid_card.delete() nb_raid_card.delete()
# create card that are not in netbox # create card that are not in netbox
@ -304,25 +310,32 @@ class Inventory():
self.create_netbox_raid_card(raid_card) self.create_netbox_raid_card(raid_card)
def is_virtual_disk(self, disk, raid_devices): def is_virtual_disk(self, disk, raid_devices):
disk_type = disk.get('type') disk_type = disk.get("type")
logicalname = disk.get('logicalname') logicalname = disk.get("logicalname")
description = disk.get('description') description = disk.get("description")
size = disk.get('size') size = disk.get("size")
product = disk.get('product') product = disk.get("product")
if logicalname in raid_devices or disk_type is None or product is None or description is None: if (
logicalname in raid_devices
or disk_type is None
or product is None
or description is None
):
return True return True
non_raid_disks = [ non_raid_disks = [
'MR9361-8i', "MR9361-8i",
] ]
if logicalname in raid_devices or \ if (
product in non_raid_disks or \ logicalname in raid_devices
'virtual' in product.lower() or \ or product in non_raid_disks
'logical' in product.lower() or \ or "virtual" in product.lower()
'volume' in description.lower() or \ or "logical" in product.lower()
'dvd-ram' in description.lower() or \ or "volume" in description.lower()
description == 'SCSI Enclosure' or \ or "dvd-ram" in description.lower()
(size is None and logicalname is None): or description == "SCSI Enclosure"
or (size is None and logicalname is None)
):
return True return True
return False return False
@ -333,33 +346,33 @@ class Inventory():
disks.extend(raid_card.get_physical_disks()) disks.extend(raid_card.get_physical_disks())
raid_devices = [ raid_devices = [
d.get('custom_fields', {}).get('vd_device') d.get("custom_fields", {}).get("vd_device")
for d in disks for d in disks
if d.get('custom_fields', {}).get('vd_device') if d.get("custom_fields", {}).get("vd_device")
] ]
for disk in self.lshw.get_hw_linux("storage"): for disk in self.lshw.get_hw_linux("storage"):
if self.is_virtual_disk(disk, raid_devices): if self.is_virtual_disk(disk, raid_devices):
continue continue
size = int(getattr(disk, "size", 0)) / 1073741824 size = round(int(disk.get("size", 0)) / 1073741824, 1)
d = { d = {
"name": "", "name": "",
'Size': '{} GB'.format(size), "Size": "{} GB".format(size),
'logicalname': disk.get('logicalname'), "logicalname": disk.get("logicalname"),
'description': disk.get('description'), "description": disk.get("description"),
'SN': disk.get('serial'), "SN": disk.get("serial"),
'Model': disk.get('product'), "Model": disk.get("product"),
'Type': disk.get('type'), "Type": disk.get("type"),
} }
if disk.get('vendor'): if disk.get("vendor"):
d['Vendor'] = disk['vendor'] d["Vendor"] = disk["vendor"]
else: else:
d['Vendor'] = get_vendor(disk['product']) d["Vendor"] = get_vendor(disk["product"])
disks.append(d) disks.append(d)
# remove duplicate serials # remove duplicate serials
seen = set() seen = set()
uniq = [x for x in disks if x['SN'] not in seen and not seen.add(x['SN'])] uniq = [x for x in disks if x["SN"] not in seen and not seen.add(x["SN"])]
return uniq return uniq
def create_netbox_disk(self, disk): def create_netbox_disk(self, disk):
@ -367,53 +380,45 @@ class Inventory():
if "Vendor" in disk: if "Vendor" in disk:
manufacturer = self.find_or_create_manufacturer(disk["Vendor"]) manufacturer = self.find_or_create_manufacturer(disk["Vendor"])
logicalname = disk.get('logicalname') name = "{} ({})".format(disk["Model"], disk["Size"])
desc = disk.get('description') description = disk["Type"]
name = '{} ({})'.format(disk['Model'], disk['Size']) sn = disk.get("SN", "unknown")
description = disk['Type']
sn = disk.get('SN', 'unknown')
parms = { parms = {
'device': self.device_id, "device": self.device_id,
'discovered': True, "discovered": True,
'tags': [{'name': INVENTORY_TAG['disk']['name']}], "tags": [{"name": INVENTORY_TAG["disk"]["name"]}],
'name': name, "name": name,
'serial': sn, "serial": sn,
'part_id': disk['Model'], "part_id": disk["Model"],
'description': description, "description": description,
'manufacturer': getattr(manufacturer, "id", None), "manufacturer": getattr(manufacturer, "id", None),
} }
if config.process_virtual_drives: if config.process_virtual_drives:
parms['custom_fields'] = disk.get("custom_fields", {}) parms["custom_fields"] = disk.get("custom_fields", {})
_ = nb.dcim.inventory_items.create(**parms) _ = nb.dcim.inventory_items.create(**parms)
logging.info('Creating Disk {model} {serial}'.format( logging.info(
model=disk['Model'], "Creating Disk {model} {serial}".format(
model=disk["Model"],
serial=sn, serial=sn,
)) )
)
def dump_disks_map(self, disks): def dump_disks_map(self, disks):
disk_map = [d['custom_fields'] for d in disks if 'custom_fields' in d] disk_map = [d["custom_fields"] for d in disks if "custom_fields" in d]
if config.dump_disks_map == "-": if config.dump_disks_map == "-":
f = sys.stdout f = sys.stdout
else: else:
f = open(config.dump_disks_map, "w") f = open(config.dump_disks_map, "w")
f.write( f.write(json.dumps(disk_map, separators=(",", ":"), indent=4, sort_keys=True))
json.dumps(
disk_map,
separators=(',', ':'),
indent=4,
sort_keys=True
)
)
if config.dump_disks_map != "-": if config.dump_disks_map != "-":
f.close() f.close()
def do_netbox_disks(self): def do_netbox_disks(self):
nb_disks = self.get_netbox_inventory( nb_disks = self.get_netbox_inventory(
device_id=self.device_id, device_id=self.device_id, tag=INVENTORY_TAG["disk"]["slug"]
tag=INVENTORY_TAG['disk']['slug']
) )
disks = self.get_hw_disks() disks = self.get_hw_disks()
if config.dump_disks_map: if config.dump_disks_map:
@ -422,100 +427,108 @@ class Inventory():
except Exception as e: except Exception as e:
logging.error("Failed to dump disks map: {}".format(e)) logging.error("Failed to dump disks map: {}".format(e))
logging.debug(traceback.format_exc()) logging.debug(traceback.format_exc())
disk_serials = [d['SN'] for d in disks if 'SN' in d] disk_serials = [d["SN"] for d in disks if "SN" in d]
# delete disks that are in netbox but not locally # delete disks that are in netbox but not locally
# use the serial_number has the comparison element # use the serial_number has the comparison element
for nb_disk in nb_disks: for nb_disk in nb_disks:
if nb_disk.serial not in disk_serials or \ if nb_disk.serial not in disk_serials or config.force_disk_refresh:
config.force_disk_refresh: logging.info(
logging.info('Deleting unknown locally Disk {serial}'.format( "Deleting unknown locally Disk {serial}".format(
serial=nb_disk.serial, serial=nb_disk.serial,
)) )
)
nb_disk.delete() nb_disk.delete()
if config.force_disk_refresh: if config.force_disk_refresh:
nb_disks = self.get_netbox_inventory( nb_disks = self.get_netbox_inventory(
device_id=self.device_id, device_id=self.device_id, tag=INVENTORY_TAG["disk"]["slug"]
tag=INVENTORY_TAG['disk']['slug']
) )
# create disks that are not in netbox # create disks that are not in netbox
for disk in disks: for disk in disks:
if disk.get('SN') not in [d.serial for d in nb_disks]: if disk.get("SN") not in [d.serial for d in nb_disks]:
self.create_netbox_disk(disk) self.create_netbox_disk(disk)
def create_netbox_memory(self, memory): def create_netbox_memory(self, memory):
manufacturer = self.find_or_create_manufacturer(memory['vendor']) manufacturer = self.find_or_create_manufacturer(memory["vendor"])
name = 'Slot {} ({}GB)'.format(memory['slot'], memory['size']) name = "Slot {} ({}GB)".format(memory["slot"], memory["size"])
nb_memory = nb.dcim.inventory_items.create( nb_memory = nb.dcim.inventory_items.create(
device=self.device_id, device=self.device_id,
discovered=True, discovered=True,
manufacturer=manufacturer.id, manufacturer=manufacturer.id,
tags=[{'name': INVENTORY_TAG['memory']['name']}], tags=[{"name": INVENTORY_TAG["memory"]["name"]}],
name=name, name=name,
part_id=memory['product'], part_id=memory["product"],
serial=memory['serial'], serial=memory["serial"],
description=memory['description'], description=memory["description"],
) )
logging.info('Creating Memory {location} {type} {size}GB'.format( logging.info(
location=memory['slot'], "Creating Memory {location} {type} {size}GB".format(
type=memory['product'], location=memory["slot"],
size=memory['size'], type=memory["product"],
)) size=memory["size"],
)
)
return nb_memory return nb_memory
def do_netbox_memories(self): def do_netbox_memories(self):
memories = self.lshw.memories memories = self.lshw.memories
nb_memories = self.get_netbox_inventory( nb_memories = self.get_netbox_inventory(
device_id=self.device_id, device_id=self.device_id, tag=INVENTORY_TAG["memory"]["slug"]
tag=INVENTORY_TAG['memory']['slug']
) )
for nb_memory in nb_memories: for nb_memory in nb_memories:
if nb_memory.serial not in [x['serial'] for x in memories]: if nb_memory.serial not in [x["serial"] for x in memories]:
logging.info('Deleting unknown locally Memory {serial}'.format( logging.info(
"Deleting unknown locally Memory {serial}".format(
serial=nb_memory.serial, serial=nb_memory.serial,
)) )
)
nb_memory.delete() nb_memory.delete()
for memory in memories: for memory in memories:
if memory.get('serial') not in [x.serial for x in nb_memories]: if memory.get("serial") not in [x.serial for x in nb_memories]:
self.create_netbox_memory(memory) self.create_netbox_memory(memory)
def create_netbox_gpus(self, gpus): def create_netbox_gpus(self, gpus):
for gpu in gpus: for gpu in gpus:
if 'product' in gpu and len(gpu['product']) > 50: if "product" in gpu and len(gpu["product"]) > 50:
gpu['product'] = (gpu['product'][:48] + '..') gpu["product"] = gpu["product"][:48] + ".."
manufacturer = self.find_or_create_manufacturer(gpu["vendor"]) manufacturer = self.find_or_create_manufacturer(gpu["vendor"])
_ = nb.dcim.inventory_items.create( _ = nb.dcim.inventory_items.create(
device=self.device_id, device=self.device_id,
manufacturer=manufacturer.id, manufacturer=manufacturer.id,
discovered=True, discovered=True,
tags=[{'name': INVENTORY_TAG['gpu']['name']}], tags=[{"name": INVENTORY_TAG["gpu"]["name"]}],
name=gpu['product'], name=gpu["product"],
description=gpu['description'], description=gpu["description"],
) )
logging.info('Creating GPU model {}'.format(gpu['product'])) logging.info("Creating GPU model {}".format(gpu["product"]))
def is_external_gpu(self, gpu): def is_external_gpu(self, gpu):
is_3d_gpu = gpu['description'].startswith('3D') is_3d_gpu = gpu["description"].startswith("3D")
return self.server.is_blade() and \ return (
self.server.own_gpu_expansion_slot() and is_3d_gpu self.server.is_blade()
and self.server.own_gpu_expansion_slot()
and is_3d_gpu
)
def do_netbox_gpus(self): def do_netbox_gpus(self):
gpus = [] gpus = []
gpu_models = {} gpu_models = {}
for gpu in self.lshw.get_hw_linux('gpu'): for gpu in self.lshw.get_hw_linux("gpu"):
# Filters GPU if an expansion bay is detected: # Filters GPU if an expansion bay is detected:
# The internal (VGA) GPU only goes into the blade inventory, # The internal (VGA) GPU only goes into the blade inventory,
# the external (3D) GPU goes into the expansion blade. # the external (3D) GPU goes into the expansion blade.
if config.expansion_as_device and \ if (
self.update_expansion ^ self.is_external_gpu(gpu): config.expansion_as_device
and self.update_expansion ^ self.is_external_gpu(gpu)
):
continue continue
gpus.append(gpu) gpus.append(gpu)
gpu_models.setdefault(gpu["product"], 0) gpu_models.setdefault(gpu["product"], 0)
@ -523,7 +536,7 @@ class Inventory():
nb_gpus = self.get_netbox_inventory( nb_gpus = self.get_netbox_inventory(
device_id=self.device_id, device_id=self.device_id,
tag=INVENTORY_TAG['gpu']['slug'], tag=INVENTORY_TAG["gpu"]["slug"],
) )
nb_gpu_models = {} nb_gpu_models = {}
for gpu in nb_gpus: for gpu in nb_gpus:

View file

@ -4,7 +4,7 @@ import subprocess
from netaddr import IPNetwork from netaddr import IPNetwork
class IPMI(): class IPMI:
""" """
Parse IPMI output Parse IPMI output
ie: ie:
@ -37,35 +37,41 @@ class IPMI():
""" """
def __init__(self): def __init__(self):
self.ret, self.output = subprocess.getstatusoutput('ipmitool lan print') self.ret, self.output = subprocess.getstatusoutput("ipmitool lan print")
if self.ret != 0: if self.ret != 0:
logging.warning('IPMI command failed: {}'.format(self.output)) logging.error("Cannot get ipmi info: {}".format(self.output))
def parse(self): def parse(self):
_ipmi = {} _ipmi = {}
if self.ret != 0:
return _ipmi
for line in self.output.splitlines(): for line in self.output.splitlines():
key = line.split(':')[0].strip() key = line.split(":")[0].strip()
if key not in ['802.1q VLAN ID', 'IP Address', 'Subnet Mask', 'MAC Address']: if key not in [
"802.1q VLAN ID",
"IP Address",
"Subnet Mask",
"MAC Address",
]:
continue continue
value = ':'.join(line.split(':')[1:]).strip() value = ":".join(line.split(":")[1:]).strip()
_ipmi[key] = value _ipmi[key] = value
ret = {} ret = {}
ret['name'] = 'IPMI' ret["name"] = "IPMI"
ret["mtu"] = 1500 ret["mtu"] = 1500
ret['bonding'] = False ret["bonding"] = False
try: ret["mac"] = _ipmi["MAC Address"]
ret['mac'] = _ipmi['MAC Address'] ret["vlan"] = (
ret['vlan'] = int(_ipmi['802.1q VLAN ID']) \ int(_ipmi["802.1q VLAN ID"])
if _ipmi['802.1q VLAN ID'] != 'Disabled' else None if _ipmi["802.1q VLAN ID"] != "Disabled"
ip = _ipmi['IP Address'] else None
netmask = _ipmi['Subnet Mask'] )
except KeyError as e: ip = _ipmi["IP Address"]
logging.error("IPMI decoding failed, missing: ", e.args[0]) netmask = _ipmi["Subnet Mask"]
return {} address = str(IPNetwork("{}/{}".format(ip, netmask)))
address = str(IPNetwork('{}/{}'.format(ip, netmask)))
ret['ip'] = [address] ret["ip"] = [address]
ret['ipmi'] = True ret["ipmi"] = True
return ret return ret

View file

@ -4,14 +4,14 @@ import subprocess
from netbox_agent.misc import is_tool from netbox_agent.misc import is_tool
class LLDP(): class LLDP:
def __init__(self, output=None): def __init__(self, output=None):
if not is_tool('lldpctl'): if not is_tool("lldpctl"):
logging.debug('lldpd package seems to be missing or daemon not running.') logging.debug("lldpd package seems to be missing or daemon not running.")
if output: if output:
self.output = output self.output = output
else: else:
self.output = subprocess.getoutput('lldpctl -f keyvalue') self.output = subprocess.getoutput("lldpctl -f keyvalue")
self.data = self.parse() self.data = self.parse()
def parse(self): def parse(self):
@ -19,7 +19,7 @@ class LLDP():
vlans = {} vlans = {}
vid = None vid = None
for entry in self.output.splitlines(): for entry in self.output.splitlines():
if '=' not in entry: if "=" not in entry:
continue continue
path, value = entry.strip().split("=", 1) path, value = entry.strip().split("=", 1)
split_path = path.split(".") split_path = path.split(".")
@ -34,38 +34,38 @@ class LLDP():
if not isinstance(current_dict.get(path_component), dict): if not isinstance(current_dict.get(path_component), dict):
current_dict[path_component] = {} current_dict[path_component] = {}
current_dict = current_dict.get(path_component) current_dict = current_dict.get(path_component)
if 'vlan-id' in path: if "vlan-id" in path:
vid = value vid = value
vlans[interface][value] = vlans[interface].get(vid, {}) vlans[interface][value] = vlans[interface].get(vid, {})
elif path.endswith('vlan'): elif path.endswith("vlan"):
vid = value.replace('vlan-', '') vid = value.replace("vlan-", "")
vlans[interface][vid] = vlans[interface].get(vid, {}) vlans[interface][vid] = vlans[interface].get(vid, {})
elif 'pvid' in path: elif "pvid" in path:
vlans[interface][vid]['pvid'] = True vlans[interface][vid]["pvid"] = True
if 'vlan' not in path: if "vlan" not in path:
current_dict[final] = value current_dict[final] = value
for interface, vlan in vlans.items(): for interface, vlan in vlans.items():
output_dict['lldp'][interface]['vlan'] = vlan output_dict["lldp"][interface]["vlan"] = vlan
if not output_dict: if not output_dict:
logging.debug('No LLDP output, please check your network config.') logging.debug("No LLDP output, please check your network config.")
return output_dict return output_dict
def get_switch_ip(self, interface): def get_switch_ip(self, interface):
# lldp.eth0.chassis.mgmt-ip=100.66.7.222 # lldp.eth0.chassis.mgmt-ip=100.66.7.222
if self.data['lldp'].get(interface) is None: if self.data["lldp"].get(interface) is None:
return None return None
return self.data['lldp'][interface]['chassis'].get('mgmt-ip') return self.data["lldp"][interface]["chassis"].get("mgmt-ip")
def get_switch_port(self, interface): def get_switch_port(self, interface):
# lldp.eth0.port.descr=GigabitEthernet1/0/1 # lldp.eth0.port.descr=GigabitEthernet1/0/1
if self.data['lldp'].get(interface) is None: if self.data["lldp"].get(interface) is None:
return None return None
if self.data['lldp'][interface]['port'].get('ifname'): if self.data["lldp"][interface]["port"].get("ifname"):
return self.data['lldp'][interface]['port']['ifname'] return self.data["lldp"][interface]["port"]["ifname"]
return self.data['lldp'][interface]['port']['descr'] return self.data["lldp"][interface]["port"]["descr"]
def get_switch_vlan(self, interface): def get_switch_vlan(self, interface):
# lldp.eth0.vlan.vlan-id=296 # lldp.eth0.vlan.vlan-id=296
if self.data['lldp'].get(interface) is None: if self.data["lldp"].get(interface) is None:
return None return None
return self.data['lldp'][interface]['vlan'] return self.data["lldp"][interface]["vlan"]

View file

@ -4,7 +4,7 @@ import importlib.machinery
from netbox_agent.config import config from netbox_agent.config import config
class LocationBase(): class LocationBase:
""" """
This class is used to guess the location in order to push the information This class is used to guess the location in order to push the information
in Netbox for a `Device` in Netbox for a `Device`
@ -27,15 +27,19 @@ class LocationBase():
if self.driver_file: if self.driver_file:
try: try:
# FIXME: Works with Python 3.3+, support older version? # FIXME: Works with Python 3.3+, support older version?
loader = importlib.machinery.SourceFileLoader('driver_file', self.driver_file) loader = importlib.machinery.SourceFileLoader(
"driver_file", self.driver_file
)
self.driver = loader.load_module() self.driver = loader.load_module()
except ImportError: except ImportError:
raise ImportError("Couldn't import {} as a module".format(self.driver_file)) raise ImportError(
"Couldn't import {} as a module".format(self.driver_file)
)
else: else:
if self.driver: if self.driver:
try: try:
self.driver = importlib.import_module( self.driver = importlib.import_module(
'netbox_agent.drivers.{}'.format(self.driver) "netbox_agent.drivers.{}".format(self.driver)
) )
except ImportError: except ImportError:
raise ImportError("Driver {} doesn't exists".format(self.driver)) raise ImportError("Driver {} doesn't exists".format(self.driver))
@ -43,19 +47,23 @@ class LocationBase():
def get(self): def get(self):
if self.driver is None: if self.driver is None:
return None return None
if not hasattr(self.driver, 'get'): if not hasattr(self.driver, "get"):
raise Exception( raise Exception(
"Your driver {} doesn't have a get() function, please fix it".format(self.driver) "Your driver {} doesn't have a get() function, please fix it".format(
self.driver
) )
return getattr(self.driver, 'get')(self.driver_value, self.regex) )
return getattr(self.driver, "get")(self.driver_value, self.regex)
class Tenant(LocationBase): class Tenant(LocationBase):
def __init__(self): def __init__(self):
driver = config.tenant.driver.split(':')[0] if \ driver = config.tenant.driver.split(":")[0] if config.tenant.driver else None
config.tenant.driver else None driver_value = (
driver_value = ':'.join(config.tenant.driver.split(':')[1:]) if \ ":".join(config.tenant.driver.split(":")[1:])
config.tenant.driver else None if config.tenant.driver
else None
)
driver_file = config.tenant.driver_file driver_file = config.tenant.driver_file
regex = config.tenant.regex regex = config.tenant.regex
super().__init__(driver, driver_value, driver_file, regex) super().__init__(driver, driver_value, driver_file, regex)
@ -63,10 +71,16 @@ class Tenant(LocationBase):
class Datacenter(LocationBase): class Datacenter(LocationBase):
def __init__(self): def __init__(self):
driver = config.datacenter_location.driver.split(':')[0] if \ driver = (
config.datacenter_location.driver else None config.datacenter_location.driver.split(":")[0]
driver_value = ':'.join(config.datacenter_location.driver.split(':')[1:]) if \ if config.datacenter_location.driver
config.datacenter_location.driver else None else None
)
driver_value = (
":".join(config.datacenter_location.driver.split(":")[1:])
if config.datacenter_location.driver
else None
)
driver_file = config.datacenter_location.driver_file driver_file = config.datacenter_location.driver_file
regex = config.datacenter_location.regex regex = config.datacenter_location.regex
super().__init__(driver, driver_value, driver_file, regex) super().__init__(driver, driver_value, driver_file, regex)
@ -74,10 +88,16 @@ class Datacenter(LocationBase):
class Rack(LocationBase): class Rack(LocationBase):
def __init__(self): def __init__(self):
driver = config.rack_location.driver.split(':')[0] if \ driver = (
config.rack_location.driver else None config.rack_location.driver.split(":")[0]
driver_value = ':'.join(config.rack_location.driver.split(':')[1:]) if \ if config.rack_location.driver
config.rack_location.driver else None else None
)
driver_value = (
":".join(config.rack_location.driver.split(":")[1:])
if config.rack_location.driver
else None
)
driver_file = config.rack_location.driver_file driver_file = config.rack_location.driver_file
regex = config.rack_location.regex regex = config.rack_location.regex
super().__init__(driver, driver_value, driver_file, regex) super().__init__(driver, driver_value, driver_file, regex)
@ -85,10 +105,16 @@ class Rack(LocationBase):
class Slot(LocationBase): class Slot(LocationBase):
def __init__(self): def __init__(self):
driver = config.slot_location.driver.split(':')[0] if \ driver = (
config.slot_location.driver else None config.slot_location.driver.split(":")[0]
driver_value = ':'.join(config.slot_location.driver.split(':')[1:]) if \ if config.slot_location.driver
config.slot_location.driver else None else None
)
driver_value = (
":".join(config.slot_location.driver.split(":")[1:])
if config.slot_location.driver
else None
)
driver_file = config.slot_location.driver_file driver_file = config.slot_location.driver_file
regex = config.slot_location.regex regex = config.slot_location.regex
super().__init__(driver, driver_value, driver_file, regex) super().__init__(driver, driver_value, driver_file, regex)

View file

@ -3,7 +3,7 @@ import logging
from netbox_agent.config import config from netbox_agent.config import config
logger = logging.getLogger() logger = logging.getLogger()
if config.log_level.lower() == 'debug': if config.log_level.lower() == "debug":
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
else: else:
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)

View file

@ -1,19 +1,18 @@
from netbox_agent.misc import is_tool
import subprocess
import logging
import json import json
import logging
import subprocess
import sys import sys
from netbox_agent.misc import is_tool
class LSHW():
class LSHW:
def __init__(self): def __init__(self):
if not is_tool('lshw'): if not is_tool("lshw"):
logging.error('lshw does not seem to be installed') logging.error("lshw does not seem to be installed")
sys.exit(1) sys.exit(1)
data = subprocess.getoutput( data = subprocess.getoutput("lshw -quiet -json")
'lshw -quiet -json'
)
json_data = json.loads(data) json_data = json.loads(data)
# Starting from version 02.18, `lshw -json` wraps its result in a list # Starting from version 02.18, `lshw -json` wraps its result in a list
# rather than returning directly a dictionary # rather than returning directly a dictionary
@ -63,9 +62,9 @@ class LSHW():
return self.gpus return self.gpus
if hwclass == "network": if hwclass == "network":
return self.interfaces return self.interfaces
if hwclass == 'storage': if hwclass == "storage":
return self.disks return self.disks
if hwclass == 'memory': if hwclass == "memory":
return self.memories return self.memories
def find_network(self, obj): def find_network(self, obj):
@ -77,26 +76,29 @@ class LSHW():
# newer versions of lshw can return a list of names, see issue #227 # newer versions of lshw can return a list of names, see issue #227
if not isinstance(i["name"], list): if not isinstance(i["name"], list):
if i["name"].startswith("unknown"): if i["name"].startswith("unknown"):
unkn_intfs.push(i) unkn_intfs.append(i)
else: else:
for j in i["name"]: for j in i["name"]:
if j.startswith("unknown"): if j.startswith("unknown"):
unkn_intfs.push(j) unkn_intfs.append(j)
unkn_name = "unknown{}".format(len(unkn_intfs)) unkn_name = "unknown{}".format(len(unkn_intfs))
self.interfaces.append({ self.interfaces.append(
{
"name": obj.get("logicalname", unkn_name), "name": obj.get("logicalname", unkn_name),
"macaddress": obj.get("serial", ""), "macaddress": obj.get("serial", ""),
"serial": obj.get("serial", ""), "serial": obj.get("serial", ""),
"product": obj["product"], "product": obj.get("product", "Unknown NIC"),
"vendor": obj["vendor"], "vendor": obj.get("vendor", "Unknown"),
"description": obj["description"], "description": obj.get("description", ""),
}) }
)
def find_storage(self, obj): def find_storage(self, obj):
if "children" in obj: if "children" in obj:
for device in obj["children"]: for device in obj["children"]:
self.disks.append({ self.disks.append(
{
"logicalname": device.get("logicalname"), "logicalname": device.get("logicalname"),
"product": device.get("product"), "product": device.get("product"),
"serial": device.get("serial"), "serial": device.get("serial"),
@ -104,42 +106,45 @@ class LSHW():
"size": device.get("size"), "size": device.get("size"),
"description": device.get("description"), "description": device.get("description"),
"type": device.get("description"), "type": device.get("description"),
}) }
)
elif "nvme" in obj["configuration"]["driver"]: elif "nvme" in obj["configuration"]["driver"]:
if not is_tool('nvme'): if not is_tool("nvme"):
logging.error('nvme-cli >= 1.0 does not seem to be installed') logging.error("nvme-cli >= 1.0 does not seem to be installed")
return return
try: try:
nvme = json.loads( nvme = json.loads(
subprocess.check_output( subprocess.check_output(
["nvme", '-list', '-o', 'json'], ["nvme", "-list", "-o", "json"], encoding="utf8"
encoding='utf8') )
) )
for device in nvme["Devices"]: for device in nvme["Devices"]:
d = { d = {
'logicalname': device["DevicePath"], "logicalname": device["DevicePath"],
'product': device["ModelNumber"], "product": device["ModelNumber"],
'serial': device["SerialNumber"], "serial": device["SerialNumber"],
"version": device["Firmware"], "version": device["Firmware"],
'description': "NVME", "description": "NVME",
'type': "NVME", "type": "NVME",
} }
if "UsedSize" in device: if "UsedSize" in device:
d['size'] = device["UsedSize"] d["size"] = device["UsedSize"]
if "UsedBytes" in device: if "UsedBytes" in device:
d['size'] = device["UsedBytes"] d["size"] = device["UsedBytes"]
self.disks.append(d) self.disks.append(d)
except Exception: except Exception:
pass pass
def find_cpus(self, obj): def find_cpus(self, obj):
if "product" in obj: if "product" in obj:
self.cpus.append({ self.cpus.append(
"product": obj["product"], {
"vendor": obj["vendor"], "product": obj.get("product", "Unknown CPU"),
"description": obj["description"], "vendor": obj.get("vendor", "Unknown vendor"),
"location": obj["slot"], "description": obj.get("description", ""),
}) "location": obj.get("slot", ""),
}
)
def find_memories(self, obj): def find_memories(self, obj):
if "children" not in obj: if "children" not in obj:
@ -150,23 +155,26 @@ class LSHW():
if "empty" in dimm["description"]: if "empty" in dimm["description"]:
continue continue
self.memories.append({ self.memories.append(
{
"slot": dimm.get("slot"), "slot": dimm.get("slot"),
"description": dimm.get("description"), "description": dimm.get("description"),
"id": dimm.get("id"), "id": dimm.get("id"),
"serial": dimm.get("serial", 'N/A'), "serial": dimm.get("serial", "N/A"),
"vendor": dimm.get("vendor", 'N/A'), "vendor": dimm.get("vendor", "N/A"),
"product": dimm.get("product", 'N/A'), "product": dimm.get("product", "N/A"),
"size": dimm.get("size", 0) / 2**20 / 1024, "size": dimm.get("size", 0) / 2**20 / 1024,
}) }
)
def find_gpus(self, obj): def find_gpus(self, obj):
if "product" in obj: if "product" in obj:
self.gpus.append({ infos = {
"product": obj["product"], "product": obj.get("product", "Unknown GPU"),
"vendor": obj["vendor"], "vendor": obj.get("vendor", "Unknown"),
"description": obj["description"], "description": obj.get("description", ""),
}) }
self.gpus.append(infos)
def walk_bridge(self, obj): def walk_bridge(self, obj):
if "children" not in obj: if "children" not in obj:

View file

@ -1,29 +1,27 @@
from netbox_agent.config import netbox_instance as nb
from slugify import slugify
from shutil import which
import subprocess
import socket
import re import re
import socket
import subprocess
from shutil import which
from slugify import slugify
from netbox_agent.config import netbox_instance as nb
def is_tool(name): def is_tool(name):
'''Check whether `name` is on PATH and marked as executable.''' """Check whether `name` is on PATH and marked as executable."""
return which(name) is not None return which(name) is not None
def get_device_role(role): def get_device_role(role):
device_role = nb.dcim.device_roles.get( device_role = nb.dcim.device_roles.get(name=role)
name=role
)
if device_role is None: if device_role is None:
raise Exception('DeviceRole "{}" does not exist, please create it'.format(role)) raise Exception('DeviceRole "{}" does not exist, please create it'.format(role))
return device_role return device_role
def get_device_type(type): def get_device_type(type):
device_type = nb.dcim.device_types.get( device_type = nb.dcim.device_types.get(model=type)
model=type
)
if device_type is None: if device_type is None:
raise Exception('DeviceType "{}" does not exist, please create it'.format(type)) raise Exception('DeviceType "{}" does not exist, please create it'.format(type))
return device_type return device_type
@ -35,9 +33,11 @@ def get_device_platform(device_platform):
# Python 3.8+ moved linux_distribution() to distro # Python 3.8+ moved linux_distribution() to distro
try: try:
import distro import distro
linux_distribution = " ".join(distro.linux_distribution()) linux_distribution = " ".join(distro.linux_distribution())
except ImportError: except ImportError:
import platform import platform
linux_distribution = " ".join(platform.linux_distribution()) linux_distribution = " ".join(platform.linux_distribution())
if not linux_distribution: if not linux_distribution:
@ -54,24 +54,25 @@ def get_device_platform(device_platform):
) )
return device_platform return device_platform
def get_vendor(name): def get_vendor(name):
vendors = { vendors = {
'PERC': 'Dell', "PERC": "Dell",
'SANDISK': 'SanDisk', "SANDISK": "SanDisk",
'DELL': 'Dell', "DELL": "Dell",
'ST': 'Seagate', "ST": "Seagate",
'CRUCIAL': 'Crucial', "CRUCIAL": "Crucial",
'MICRON': 'Micron', "MICRON": "Micron",
'INTEL': 'Intel', "INTEL": "Intel",
'SAMSUNG': 'Samsung', "SAMSUNG": "Samsung",
'EH0': 'HP', "EH0": "HP",
'HGST': 'HGST', "HGST": "HGST",
'HUH': 'HGST', "HUH": "HGST",
'MB': 'Toshiba', "MB": "Toshiba",
'MC': 'Toshiba', "MC": "Toshiba",
'MD': 'Toshiba', "MD": "Toshiba",
'MG': 'Toshiba', "MG": "Toshiba",
'WD': 'WDC' "WD": "WDC",
} }
for key, value in vendors.items(): for key, value in vendors.items():
if name.upper().startswith(key): if name.upper().startswith(key):
@ -81,16 +82,14 @@ def get_vendor(name):
def get_hostname(config): def get_hostname(config):
if config.hostname_cmd is None: if config.hostname_cmd is None:
return '{}'.format(socket.gethostname()) return "{}".format(socket.gethostname())
return subprocess.getoutput(config.hostname_cmd) return subprocess.getoutput(config.hostname_cmd)
def create_netbox_tags(tags): def create_netbox_tags(tags):
ret = [] ret = []
for tag in tags: for tag in tags:
nb_tag = nb.extras.tags.get( nb_tag = nb.extras.tags.get(name=tag)
name=tag
)
if not nb_tag: if not nb_tag:
nb_tag = nb.extras.tags.create( nb_tag = nb.extras.tags.create(
name=tag, name=tag,
@ -102,15 +101,13 @@ def create_netbox_tags(tags):
def get_mount_points(): def get_mount_points():
mount_points = {} mount_points = {}
output = subprocess.getoutput('mount') output = subprocess.getoutput("mount")
for r in output.split("\n"): for r in output.split("\n"):
if not r.startswith("/dev/"): if not r.startswith("/dev/"):
continue continue
mount_info = r.split() mount_info = r.split()
device = mount_info[0] device = mount_info[0]
device = re.sub(r'\d+$', '', device) device = re.sub(r"\d+$", "", device)
mp = mount_info[2] mp = mount_info[2]
mount_points.setdefault(device, []).append(mp) mount_points.setdefault(device, []).append(mp)
return mount_points return mount_points

View file

@ -1,7 +1,7 @@
import logging import logging
import os import os
import re import re
from itertools import chain from itertools import chain, islice
import netifaces import netifaces
from netaddr import IPAddress from netaddr import IPAddress
@ -26,42 +26,45 @@ class Network(object):
self.dcim_choices = {} self.dcim_choices = {}
dcim_c = nb.dcim.interfaces.choices() dcim_c = nb.dcim.interfaces.choices()
for _choice_type in dcim_c: for _choice_type in dcim_c:
key = 'interface:{}'.format(_choice_type) key = "interface:{}".format(_choice_type)
self.dcim_choices[key] = {} self.dcim_choices[key] = {}
for choice in dcim_c[_choice_type]: for choice in dcim_c[_choice_type]:
self.dcim_choices[key][choice['display_name']] = choice['value'] self.dcim_choices[key][choice["display_name"]] = choice["value"]
self.ipam_choices = {} self.ipam_choices = {}
ipam_c = nb.ipam.ip_addresses.choices() ipam_c = nb.ipam.ip_addresses.choices()
for _choice_type in ipam_c: for _choice_type in ipam_c:
key = 'ip-address:{}'.format(_choice_type) key = "ip-address:{}".format(_choice_type)
self.ipam_choices[key] = {} self.ipam_choices[key] = {}
for choice in ipam_c[_choice_type]: for choice in ipam_c[_choice_type]:
self.ipam_choices[key][choice['display_name']] = choice['value'] self.ipam_choices[key][choice["display_name"]] = choice["value"]
def get_network_type(): def get_network_type():
return NotImplementedError return NotImplementedError
def scan(self): def scan(self):
nics = [] nics = []
for interface in os.listdir('/sys/class/net/'): for interface in os.listdir("/sys/class/net/"):
# ignore if it's not a link (ie: bonding_masters etc) # ignore if it's not a link (ie: bonding_masters etc)
if not os.path.islink('/sys/class/net/{}'.format(interface)): if not os.path.islink("/sys/class/net/{}".format(interface)):
continue continue
if config.network.ignore_interfaces and \ if config.network.ignore_interfaces and re.match(
re.match(config.network.ignore_interfaces, interface): config.network.ignore_interfaces, interface
logging.debug('Ignore interface {interface}'.format(interface=interface)) ):
logging.debug(
"Ignore interface {interface}".format(interface=interface)
)
continue continue
ip_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET, []) ip_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET, [])
ip6_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET6, []) ip6_addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET6, [])
if config.network.ignore_ips: if config.network.ignore_ips:
for i, ip in enumerate(ip_addr): for i, ip in enumerate(ip_addr):
if re.match(config.network.ignore_ips, ip['addr']): if re.match(config.network.ignore_ips, ip["addr"]):
ip_addr.pop(i) ip_addr.pop(i)
for i, ip in enumerate(ip6_addr): for i, ip in enumerate(ip6_addr):
if re.match(config.network.ignore_ips, ip['addr']): if re.match(config.network.ignore_ips, ip["addr"]):
ip6_addr.pop(i) ip6_addr.pop(i)
# netifaces returns a ipv6 netmask that netaddr does not understand. # netifaces returns a ipv6 netmask that netaddr does not understand.
@ -80,63 +83,72 @@ class Network(object):
# } # }
# #
for addr in ip6_addr: for addr in ip6_addr:
addr["addr"] = addr["addr"].replace('%{}'.format(interface), '') addr["addr"] = addr["addr"].replace("%{}".format(interface), "")
addr["mask"] = addr["mask"].split('/')[0] addr["mask"] = addr["mask"].split("/")[0]
ip_addr.append(addr) ip_addr.append(addr)
mac = open('/sys/class/net/{}/address'.format(interface), 'r').read().strip() mac = (
mtu = int(open('/sys/class/net/{}/mtu'.format(interface), 'r').read().strip()) open("/sys/class/net/{}/address".format(interface), "r").read().strip()
)
mtu = int(
open("/sys/class/net/{}/mtu".format(interface), "r").read().strip()
)
vlan = None vlan = None
if len(interface.split('.')) > 1: if len(interface.split(".")) > 1:
vlan = int(interface.split('.')[1]) vlan = int(interface.split(".")[1])
bonding = False bonding = False
bonding_slaves = [] bonding_slaves = []
if os.path.isdir('/sys/class/net/{}/bonding'.format(interface)): if os.path.isdir("/sys/class/net/{}/bonding".format(interface)):
bonding = True bonding = True
bonding_slaves = open( bonding_slaves = (
'/sys/class/net/{}/bonding/slaves'.format(interface) open("/sys/class/net/{}/bonding/slaves".format(interface))
).read().split() .read()
.split()
# Tun and TAP support
virtual = os.path.isfile(
'/sys/class/net/{}/tun_flags'.format(interface)
) )
# Tun and TAP support
virtual = os.path.isfile("/sys/class/net/{}/tun_flags".format(interface))
nic = { nic = {
'name': interface, "name": interface,
'mac': mac if mac != '00:00:00:00:00:00' else None, "mac": mac if mac != "00:00:00:00:00:00" else None,
'ip': [ "ip": (
'{}/{}'.format( [
x['addr'], "{}/{}".format(x["addr"], IPAddress(x["mask"]).netmask_bits())
IPAddress(x['mask']).netmask_bits() for x in ip_addr
) for x in ip_addr ]
] if ip_addr else None, # FIXME: handle IPv6 addresses if ip_addr
'ethtool': Ethtool(interface).parse(), else None
'virtual': virtual, ), # FIXME: handle IPv6 addresses
'vlan': vlan, "ethtool": Ethtool(interface).parse(),
'mtu': mtu, "virtual": virtual,
'bonding': bonding, "vlan": vlan,
'bonding_slaves': bonding_slaves, "mtu": mtu,
"bonding": bonding,
"bonding_slaves": bonding_slaves,
} }
nics.append(nic) nics.append(nic)
return nics return nics
def _set_bonding_interfaces(self): def _set_bonding_interfaces(self):
bonding_nics = (x for x in self.nics if x['bonding']) bonding_nics = (x for x in self.nics if x["bonding"])
for nic in bonding_nics: for nic in bonding_nics:
bond_int = self.get_netbox_network_card(nic) bond_int = self.get_netbox_network_card(nic)
logging.debug('Setting slave interface for {name}'.format( logging.debug(
name=bond_int.name "Setting slave interface for {name}".format(name=bond_int.name)
)) )
for slave_int in ( for slave_int in (
self.get_netbox_network_card(slave_nic) self.get_netbox_network_card(slave_nic)
for slave_nic in self.nics for slave_nic in self.nics
if slave_nic['name'] in nic['bonding_slaves']): if slave_nic["name"] in nic["bonding_slaves"]
):
if slave_int.lag is None or slave_int.lag.id != bond_int.id: if slave_int.lag is None or slave_int.lag.id != bond_int.id:
logging.debug('Settting interface {name} as slave of {master}'.format( logging.debug(
"Settting interface {name} as slave of {master}".format(
name=slave_int.name, master=bond_int.name name=slave_int.name, master=bond_int.name
)) )
)
slave_int.lag = bond_int slave_int.lag = bond_int
slave_int.save() slave_int.save()
else: else:
@ -147,55 +159,50 @@ class Network(object):
return self.nics return self.nics
def get_netbox_network_card(self, nic): def get_netbox_network_card(self, nic):
if nic['mac'] is None: if nic["mac"] is None:
interface = self.nb_net.interfaces.get( interface = self.nb_net.interfaces.get(
name=nic['name'], name=nic["name"], **self.custom_arg_id
**self.custom_arg_id
) )
else: else:
interface = self.nb_net.interfaces.get( interface = self.nb_net.interfaces.get(
mac_address=nic['mac'], mac_address=nic["mac"], name=nic["name"], **self.custom_arg_id
name=nic['name'],
**self.custom_arg_id
) )
return interface return interface
def get_netbox_network_cards(self): def get_netbox_network_cards(self):
return self.nb_net.interfaces.filter( return self.nb_net.interfaces.filter(**self.custom_arg_id)
**self.custom_arg_id
)
def get_netbox_type_for_nic(self, nic): def get_netbox_type_for_nic(self, nic):
if self.get_network_type() == 'virtual': if self.get_network_type() == "virtual":
return self.dcim_choices['interface:type']['Virtual'] return self.dcim_choices["interface:type"]["Virtual"]
if nic.get('bonding'): if nic.get("bonding"):
return self.dcim_choices['interface:type']['Link Aggregation Group (LAG)'] return self.dcim_choices["interface:type"]["Link Aggregation Group (LAG)"]
if nic.get('bonding'): if nic.get("bonding"):
return self.dcim_choices['interface:type']['Link Aggregation Group (LAG)'] return self.dcim_choices["interface:type"]["Link Aggregation Group (LAG)"]
if nic.get('virtual'): if nic.get("virtual"):
return self.dcim_choices['interface:type']['Virtual'] return self.dcim_choices["interface:type"]["Virtual"]
if nic.get('ethtool') is None: if nic.get("ethtool") is None:
return self.dcim_choices['interface:type']['Other'] return self.dcim_choices["interface:type"]["Other"]
if nic['ethtool']['speed'] == '10000Mb/s': if nic["ethtool"]["speed"] == "10000Mb/s":
if nic['ethtool']['port'] in ('FIBRE', 'Direct Attach Copper'): if nic["ethtool"]["port"] in ("FIBRE", "Direct Attach Copper"):
return self.dcim_choices['interface:type']['SFP+ (10GE)'] return self.dcim_choices["interface:type"]["SFP+ (10GE)"]
return self.dcim_choices['interface:type']['10GBASE-T (10GE)'] return self.dcim_choices["interface:type"]["10GBASE-T (10GE)"]
elif nic['ethtool']['speed'] == '25000Mb/s': elif nic["ethtool"]["speed"] == "25000Mb/s":
if nic['ethtool']['port'] in ('FIBRE', 'Direct Attach Copper'): if nic["ethtool"]["port"] in ("FIBRE", "Direct Attach Copper"):
return self.dcim_choices['interface:type']['SFP28 (25GE)'] return self.dcim_choices["interface:type"]["SFP28 (25GE)"]
elif nic['ethtool']['speed'] == '1000Mb/s': elif nic["ethtool"]["speed"] == "1000Mb/s":
if nic['ethtool']['port'] in ('FIBRE', 'Direct Attach Copper'): if nic["ethtool"]["port"] in ("FIBRE", "Direct Attach Copper"):
return self.dcim_choices['interface:type']['SFP (1GE)'] return self.dcim_choices["interface:type"]["SFP (1GE)"]
return self.dcim_choices['interface:type']['1000BASE-T (1GE)'] return self.dcim_choices["interface:type"]["1000BASE-T (1GE)"]
return self.dcim_choices['interface:type']['Other'] return self.dcim_choices["interface:type"]["Other"]
def get_or_create_vlan(self, vlan_id): def get_or_create_vlan(self, vlan_id):
# FIXME: we may need to specify the datacenter # FIXME: we may need to specify the datacenter
@ -205,25 +212,35 @@ class Network(object):
) )
if vlan is None: if vlan is None:
vlan = nb.ipam.vlans.create( vlan = nb.ipam.vlans.create(
name='VLAN {}'.format(vlan_id), name="VLAN {}".format(vlan_id),
vid=vlan_id, vid=vlan_id,
) )
return vlan return vlan
def reset_vlan_on_interface(self, nic, interface): def reset_vlan_on_interface(self, nic, interface):
update = False update = False
vlan_id = nic['vlan'] vlan_id = nic["vlan"]
lldp_vlan = self.lldp.get_switch_vlan(nic['name']) if config.network.lldp else None lldp_vlan = (
self.lldp.get_switch_vlan(nic["name"])
if config.network.lldp and isinstance(self, ServerNetwork)
else None
)
# For strange reason, we need to get the object from scratch # 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) # The object returned by pynetbox's save isn't always working (since pynetbox 6)
interface = self.nb_net.interfaces.get(id=interface.id) interface = self.nb_net.interfaces.get(id=interface.id)
# Handle the case were the local interface isn't an interface vlan as reported by Netbox # 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 # and that LLDP doesn't report a vlan-id
if vlan_id is None and lldp_vlan is None and \ if (
(interface.mode is not None or len(interface.tagged_vlans) > 0): vlan_id is None
logging.info('Interface {interface} is not tagged, reseting mode'.format( and lldp_vlan is None
interface=interface)) and (interface.mode is not None or len(interface.tagged_vlans) > 0)
):
logging.info(
"Interface {interface} is not tagged, reseting mode".format(
interface=interface
)
)
update = True update = True
interface.mode = None interface.mode = None
interface.tagged_vlans = [] interface.tagged_vlans = []
@ -232,76 +249,92 @@ class Network(object):
# if mode is either not set or not correctly configured or vlan are not # if mode is either not set or not correctly configured or vlan are not
# correctly configured, we reset the vlan # correctly configured, we reset the vlan
elif vlan_id and ( elif vlan_id and (
interface.mode is None or interface.mode is None
type(interface.mode) is not int and ( or not isinstance(interface.mode, int)
hasattr(interface.mode, 'value') and and (
interface.mode.value == self.dcim_choices['interface:mode']['Access'] or hasattr(interface.mode, "value")
len(interface.tagged_vlans) != 1 or and interface.mode.value
int(interface.tagged_vlans[0].vid) != int(vlan_id))): == self.dcim_choices["interface:mode"]["Access"]
logging.info('Resetting tagged VLAN(s) on interface {interface}'.format( or len(interface.tagged_vlans) != 1
interface=interface)) or int(interface.tagged_vlans[0].vid) != int(vlan_id)
)
):
logging.info(
"Resetting tagged VLAN(s) on interface {interface}".format(
interface=interface
)
)
update = True update = True
nb_vlan = self.get_or_create_vlan(vlan_id) nb_vlan = self.get_or_create_vlan(vlan_id)
interface.mode = self.dcim_choices['interface:mode']['Tagged'] interface.mode = self.dcim_choices["interface:mode"]["Tagged"]
interface.tagged_vlans = [nb_vlan] if nb_vlan else [] interface.tagged_vlans = [nb_vlan] if nb_vlan else []
interface.untagged_vlan = None interface.untagged_vlan = None
# Finally if LLDP reports a vlan-id with the pvid attribute # Finally if LLDP reports a vlan-id with the pvid attribute
elif lldp_vlan: elif lldp_vlan:
pvid_vlan = [key for (key, value) in lldp_vlan.items() if value['pvid']] pvid_vlan = [key for (key, value) in lldp_vlan.items() if value["pvid"]]
if len(pvid_vlan) > 0 and ( if len(pvid_vlan) > 0 and (
interface.mode is None or interface.mode is None
interface.mode.value != self.dcim_choices['interface:mode']['Access'] or or interface.mode.value != self.dcim_choices["interface:mode"]["Access"]
interface.untagged_vlan is None or or interface.untagged_vlan is None
interface.untagged_vlan.vid != int(pvid_vlan[0])): or interface.untagged_vlan.vid != int(pvid_vlan[0])
logging.info('Resetting access VLAN on interface {interface}'.format( ):
interface=interface)) logging.info(
"Resetting access VLAN on interface {interface}".format(
interface=interface
)
)
update = True update = True
nb_vlan = self.get_or_create_vlan(pvid_vlan[0]) nb_vlan = self.get_or_create_vlan(pvid_vlan[0])
interface.mode = self.dcim_choices['interface:mode']['Access'] interface.mode = self.dcim_choices["interface:mode"]["Access"]
interface.untagged_vlan = nb_vlan.id interface.untagged_vlan = nb_vlan.id
return update, interface return update, interface
def create_netbox_nic(self, nic, mgmt=False): def create_netbox_nic(self, nic, mgmt=False):
# TODO: add Optic Vendor, PN and Serial # TODO: add Optic Vendor, PN and Serial
nic_type = self.get_netbox_type_for_nic(nic) nic_type = self.get_netbox_type_for_nic(nic)
logging.info('Creating NIC {name} ({mac}) on {device}'.format( logging.info(
name=nic['name'], mac=nic['mac'], device=self.device.name)) "Creating NIC {name} ({mac}) on {device}".format(
name=nic["name"], mac=nic["mac"], device=self.device.name
)
)
nb_vlan = None nb_vlan = None
params = dict(self.custom_arg) params = dict(self.custom_arg)
params.update({ params.update(
'name': nic['name'], {
'type': nic_type, "name": nic["name"],
'mgmt_only': mgmt, "type": nic_type,
}) "mgmt_only": mgmt,
if nic['mac']: }
params['mac_address'] = nic['mac'] )
if nic["mac"]:
params["mac_address"] = nic["mac"]
if nic['mtu']: if nic["mtu"]:
params['mtu'] = nic['mtu'] params["mtu"] = nic["mtu"]
interface = self.nb_net.interfaces.create(**params) interface = self.nb_net.interfaces.create(**params)
if nic['vlan']: if nic["vlan"]:
nb_vlan = self.get_or_create_vlan(nic['vlan']) nb_vlan = self.get_or_create_vlan(nic["vlan"])
interface.mode = self.dcim_choices['interface:mode']['Tagged'] interface.mode = self.dcim_choices["interface:mode"]["Tagged"]
interface.tagged_vlans = [nb_vlan.id] interface.tagged_vlans = [nb_vlan.id]
interface.save() interface.save()
elif config.network.lldp and self.lldp.get_switch_vlan(nic['name']) is not None: elif config.network.lldp and self.lldp.get_switch_vlan(nic["name"]) is not None:
# if lldp reports a vlan on an interface, tag the interface in access and set the vlan # if lldp reports a vlan on an interface, tag the interface in access and set the vlan
# report only the interface which has `pvid=yes` (ie: lldp.eth3.vlan.pvid=yes) # report only the interface which has `pvid=yes` (ie: lldp.eth3.vlan.pvid=yes)
# if pvid is not present, it'll be processed as a vlan tagged interface # if pvid is not present, it'll be processed as a vlan tagged interface
vlans = self.lldp.get_switch_vlan(nic['name']) vlans = self.lldp.get_switch_vlan(nic["name"])
for vid, vlan_infos in vlans.items(): for vid, vlan_infos in vlans.items():
nb_vlan = self.get_or_create_vlan(vid) nb_vlan = self.get_or_create_vlan(vid)
if vlan_infos.get('vid'): if vlan_infos.get("vid"):
interface.mode = self.dcim_choices['interface:mode']['Access'] interface.mode = self.dcim_choices["interface:mode"]["Access"]
interface.untagged_vlan = nb_vlan.id interface.untagged_vlan = nb_vlan.id
interface.save() interface.save()
# cable the interface # cable the interface
if config.network.lldp: if config.network.lldp and isinstance(self, ServerNetwork):
switch_ip = self.lldp.get_switch_ip(interface.name) switch_ip = self.lldp.get_switch_ip(interface.name)
switch_interface = self.lldp.get_switch_port(interface.name) switch_interface = self.lldp.get_switch_port(interface.name)
@ -314,7 +347,7 @@ class Network(object):
return interface return interface
def create_or_update_netbox_ip_on_interface(self, ip, interface): def create_or_update_netbox_ip_on_interface(self, ip, interface):
''' """
Two behaviors: Two behaviors:
- Anycast IP - Anycast IP
* If IP exists and is in Anycast, create a new Anycast one * If IP exists and is in Anycast, create a new Anycast one
@ -325,69 +358,78 @@ class Network(object):
* If IP doesn't exist, create it * If IP doesn't exist, create it
* If IP exists and isn't assigned, take it * If IP exists and isn't assigned, take it
* If IP exists and interface is wrong, change interface * If IP exists and interface is wrong, change interface
''' """
netbox_ips = nb.ipam.ip_addresses.filter( netbox_ips = nb.ipam.ip_addresses.filter(
address=ip, address=ip,
) )
if not netbox_ips: if not netbox_ips:
logging.info('Create new IP {ip} on {interface}'.format( logging.info(
ip=ip, interface=interface)) "Create new IP {ip} on {interface}".format(ip=ip, interface=interface)
)
query_params = { query_params = {
'address': ip, "address": ip,
'status': "active", "status": "active",
'assigned_object_type': self.assigned_object_type, "assigned_object_type": self.assigned_object_type,
'assigned_object_id': interface.id "assigned_object_id": interface.id,
} }
netbox_ip = nb.ipam.ip_addresses.create( netbox_ip = nb.ipam.ip_addresses.create(**query_params)
**query_params
)
return netbox_ip return netbox_ip
netbox_ip = list(netbox_ips)[0] netbox_ip = list(netbox_ips)[0]
# If IP exists in anycast # If IP exists in anycast
if netbox_ip.role and netbox_ip.role.label == 'Anycast': if netbox_ip.role and netbox_ip.role.label == "Anycast":
logging.debug('IP {} is Anycast..'.format(ip)) logging.debug("IP {} is Anycast..".format(ip))
unassigned_anycast_ip = [x for x in netbox_ips if x.interface is None] unassigned_anycast_ip = [x for x in netbox_ips if x.interface is None]
assigned_anycast_ip = [x for x in netbox_ips if assigned_anycast_ip = [
x.interface and x.interface.id == interface.id] x for x in netbox_ips if x.interface and x.interface.id == interface.id
]
# use the first available anycast ip # use the first available anycast ip
if len(unassigned_anycast_ip): if len(unassigned_anycast_ip):
logging.info('Assigning existing Anycast IP {} to interface'.format(ip)) logging.info("Assigning existing Anycast IP {} to interface".format(ip))
netbox_ip = unassigned_anycast_ip[0] netbox_ip = unassigned_anycast_ip[0]
netbox_ip.interface = interface netbox_ip.interface = interface
netbox_ip.save() netbox_ip.save()
# or if everything is assigned to other servers # or if everything is assigned to other servers
elif not len(assigned_anycast_ip): elif not len(assigned_anycast_ip):
logging.info('Creating Anycast IP {} and assigning it to interface'.format(ip)) logging.info(
"Creating Anycast IP {} and assigning it to interface".format(ip)
)
query_params = { query_params = {
"address": ip, "address": ip,
"status": "active", "status": "active",
"role": self.ipam_choices['ip-address:role']['Anycast'], "role": self.ipam_choices["ip-address:role"]["Anycast"],
"tenant": self.tenant.id if self.tenant else None, "tenant": self.tenant.id if self.tenant else None,
"assigned_object_type": self.assigned_object_type, "assigned_object_type": self.assigned_object_type,
"assigned_object_id": interface.id "assigned_object_id": interface.id,
} }
netbox_ip = nb.ipam.ip_addresses.create(**query_params) netbox_ip = nb.ipam.ip_addresses.create(**query_params)
return netbox_ip return netbox_ip
else: else:
ip_interface = getattr(netbox_ip, 'interface', None) ip_interface = getattr(netbox_ip, "interface", None)
assigned_object = getattr(netbox_ip, 'assigned_object', None) assigned_object = getattr(netbox_ip, "assigned_object", None)
if not ip_interface or not assigned_object: if not ip_interface or not assigned_object:
logging.info('Assigning existing IP {ip} to {interface}'.format( logging.info(
ip=ip, interface=interface)) "Assigning existing IP {ip} to {interface}".format(
elif (ip_interface and ip_interface.id != interface.id) or \ ip=ip, interface=interface
(assigned_object and assigned_object_id != interface.id): )
)
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") old_interface = getattr(netbox_ip, "assigned_object", "n/a")
logging.info( logging.info(
'Detected interface change for ip {ip}: old interface is ' "Detected interface change for ip {ip}: old interface is "
'{old_interface} (id: {old_id}), new interface is {new_interface} ' "{old_interface} (id: {old_id}), new interface is {new_interface} "
' (id: {new_id})' " (id: {new_id})".format(
.format( old_interface=old_interface,
old_interface=old_interface, new_interface=interface, new_interface=interface,
old_id=netbox_ip.id, new_id=interface.id, ip=netbox_ip.address old_id=netbox_ip.id,
)) new_id=interface.id,
ip=netbox_ip.address,
)
)
else: else:
return netbox_ip return netbox_ip
@ -398,82 +440,98 @@ class Network(object):
def create_or_update_netbox_network_cards(self): def create_or_update_netbox_network_cards(self):
if config.update_all is None or config.update_network is None: if config.update_all is None or config.update_network is None:
return None return None
logging.debug('Creating/Updating NIC...') logging.debug("Creating/Updating NIC...")
# delete unknown interface # delete unknown interface
nb_nics = list(self.get_netbox_network_cards()) nb_nics = list(self.get_netbox_network_cards())
local_nics = [x['name'] for x in self.nics] 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: if nic.name not in local_nics:
logging.info('Deleting netbox interface {name} because not present locally'.format( logging.info(
"Deleting netbox interface {name} because not present locally".format(
name=nic.name name=nic.name
)) )
)
nb_nics.remove(nic) nb_nics.remove(nic)
nic.delete() nic.delete()
# delete IP on netbox that are not known on this server # delete IP on netbox that are not known on this server
if len(nb_nics): if len(nb_nics):
netbox_ips = nb.ipam.ip_addresses.filter(
**{self.intf_type: [x.id for x in nb_nics]}
)
netbox_ips = list(netbox_ips) def batched(it, n):
all_local_ips = list(chain.from_iterable([ while batch := tuple(islice(it, n)):
x['ip'] for x in self.nics if x['ip'] is not None yield batch
]))
netbox_ips = []
for ids in batched((x.id for x in nb_nics), 25):
netbox_ips += list(nb.ipam.ip_addresses.filter(**{self.intf_type: ids}))
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: for netbox_ip in netbox_ips:
if netbox_ip.address not in all_local_ips: if netbox_ip.address not in all_local_ips:
logging.info('Unassigning IP {ip} from {interface}'.format( logging.info(
ip=netbox_ip.address, interface=netbox_ip.assigned_object)) "Unassigning IP {ip} from {interface}".format(
ip=netbox_ip.address, interface=netbox_ip.assigned_object
)
)
netbox_ip.assigned_object_type = None netbox_ip.assigned_object_type = None
netbox_ip.assigned_object_id = None netbox_ip.ssigned_object_id = None
netbox_ip.save() netbox_ip.save()
# update each nic # update each nic
for nic in self.nics: for nic in self.nics:
interface = self.get_netbox_network_card(nic) interface = self.get_netbox_network_card(nic)
if not interface: if not interface:
logging.info('Interface {mac_address} not found, creating..'.format( logging.info(
mac_address=nic['mac']) "Interface {mac_address} not found, creating..".format(
mac_address=nic["mac"]
)
) )
interface = self.create_netbox_nic(nic) interface = self.create_netbox_nic(nic)
nic_update = 0 nic_update = 0
if nic['name'] != interface.name: if nic["name"] != interface.name:
logging.info('Updating interface {interface} name to: {name}'.format( logging.info(
interface=interface, name=nic['name'])) "Updating interface {interface} name to: {name}".format(
interface.name = nic['name'] interface=interface, name=nic["name"]
)
)
interface.name = nic["name"]
nic_update += 1 nic_update += 1
ret, interface = self.reset_vlan_on_interface(nic, interface) ret, interface = self.reset_vlan_on_interface(nic, interface)
nic_update += ret nic_update += ret
if hasattr(interface, 'mtu'): if hasattr(interface, "mtu"):
if nic['mtu'] != interface.mtu: if nic["mtu"] != interface.mtu:
logging.info('Interface mtu is wrong, updating to: {mtu}'.format( logging.info(
mtu=nic['mtu'])) "Interface mtu is wrong, updating to: {mtu}".format(
interface.mtu = nic['mtu'] mtu=nic["mtu"]
)
)
interface.mtu = nic["mtu"]
nic_update += 1 nic_update += 1
if hasattr(interface, 'type'): if hasattr(interface, "type"):
_type = self.get_netbox_type_for_nic(nic) _type = self.get_netbox_type_for_nic(nic)
if not interface.type or \ if not interface.type or _type != interface.type.value:
_type != interface.type.value: logging.info("Interface type is wrong, resetting")
logging.info('Interface type is wrong, resetting')
interface.type = _type interface.type = _type
nic_update += 1 nic_update += 1
if hasattr(interface, 'lag') and interface.lag is not None: if hasattr(interface, "lag") and interface.lag is not None:
local_lag_int = next( local_lag_int = next(
item for item in self.nics if item['name'] == interface.lag.name item for item in self.nics if item["name"] == interface.lag.name
) )
if nic['name'] not in local_lag_int['bonding_slaves']: if nic["name"] not in local_lag_int["bonding_slaves"]:
logging.info('Interface has no LAG, resetting') logging.info("Interface has no LAG, resetting")
nic_update += 1 nic_update += 1
interface.lag = None interface.lag = None
# cable the interface # cable the interface
if config.network.lldp: if config.network.lldp and isinstance(self, ServerNetwork):
switch_ip = self.lldp.get_switch_ip(interface.name) switch_ip = self.lldp.get_switch_ip(interface.name)
switch_interface = self.lldp.get_switch_port(interface.name) switch_interface = self.lldp.get_switch_port(interface.name)
if switch_ip and switch_interface: if switch_ip and switch_interface:
@ -482,15 +540,15 @@ class Network(object):
) )
nic_update += ret nic_update += ret
if nic['ip']: if nic["ip"]:
# sync local IPs # sync local IPs
for ip in nic['ip']: for ip in nic["ip"]:
self.create_or_update_netbox_ip_on_interface(ip, interface) self.create_or_update_netbox_ip_on_interface(ip, interface)
if nic_update > 0: if nic_update > 0:
interface.save() interface.save()
self._set_bonding_interfaces() self._set_bonding_interfaces()
logging.debug('Finished updating NIC!') logging.debug("Finished updating NIC!")
class ServerNetwork(Network): class ServerNetwork(Network):
@ -505,38 +563,43 @@ class ServerNetwork(Network):
self.server = server self.server = server
self.device = self.server.get_netbox_server() self.device = self.server.get_netbox_server()
self.nb_net = nb.dcim self.nb_net = nb.dcim
self.custom_arg = {'device': getattr(self.device, "id", None)} self.custom_arg = {"device": getattr(self.device, "id", None)}
self.custom_arg_id = {'device_id': getattr(self.device, "id", None)} self.custom_arg_id = {"device_id": getattr(self.device, "id", None)}
self.intf_type = "interface_id" self.intf_type = "interface_id"
self.assigned_object_type = "dcim.interface" self.assigned_object_type = "dcim.interface"
def get_network_type(self): def get_network_type(self):
return 'server' return "server"
def get_ipmi(self): def get_ipmi(self):
ipmi = IPMI().parse() ipmi = IPMI().parse()
return ipmi return ipmi
def connect_interface_to_switch(self, switch_ip, switch_interface, nb_server_interface): def connect_interface_to_switch(
logging.info('Interface {} is not connected to switch, trying to connect..'.format( self, switch_ip, switch_interface, nb_server_interface
):
logging.info(
"Interface {} is not connected to switch, trying to connect..".format(
nb_server_interface.name nb_server_interface.name
)) )
)
nb_mgmt_ip = nb.ipam.ip_addresses.get( nb_mgmt_ip = nb.ipam.ip_addresses.get(
address=switch_ip, address=switch_ip,
) )
if not nb_mgmt_ip: if not nb_mgmt_ip:
logging.error('Switch IP {} cannot be found in Netbox'.format(switch_ip)) logging.error("Switch IP {} cannot be found in Netbox".format(switch_ip))
return nb_server_interface return nb_server_interface
try: try:
nb_switch = nb_mgmt_ip.assigned_object.device nb_switch = nb_mgmt_ip.assigned_object.device
logging.info('Found a switch in Netbox based on LLDP infos: {} (id: {})'.format( logging.info(
switch_ip, "Found a switch in Netbox based on LLDP infos: {} (id: {})".format(
nb_switch.id switch_ip, nb_switch.id
)) )
)
except KeyError: except KeyError:
logging.error( logging.error(
'Switch IP {} is found but not associated to a Netbox Switch Device'.format( "Switch IP {} is found but not associated to a Netbox Switch Device".format(
switch_ip switch_ip
) )
) )
@ -548,13 +611,17 @@ class ServerNetwork(Network):
name=switch_interface, name=switch_interface,
) )
if nb_switch_interface is None: if nb_switch_interface is None:
logging.error('Switch interface {} cannot be found'.format(switch_interface)) logging.error(
"Switch interface {} cannot be found".format(switch_interface)
)
return nb_server_interface return nb_server_interface
logging.info('Found interface {} on switch {}'.format( logging.info(
"Found interface {} on switch {}".format(
switch_interface, switch_interface,
switch_ip, switch_ip,
)) )
)
cable = nb.dcim.cables.create( cable = nb.dcim.cables.create(
a_terminations=[ a_terminations=[
{"object_type": "dcim.interface", "object_id": nb_server_interface.id}, {"object_type": "dcim.interface", "object_id": nb_server_interface.id},
@ -565,7 +632,7 @@ class ServerNetwork(Network):
) )
nb_server_interface.cable = cable nb_server_interface.cable = cable
logging.info( logging.info(
'Connected interface {interface} with {switch_interface} of {switch_ip}'.format( "Connected interface {interface} with {switch_interface} of {switch_ip}".format(
interface=nb_server_interface.name, interface=nb_server_interface.name,
switch_interface=switch_interface, switch_interface=switch_interface,
switch_ip=switch_ip, switch_ip=switch_ip,
@ -583,38 +650,30 @@ class ServerNetwork(Network):
else: else:
nb_sw_int = nb_server_interface.cable.b_terminations[0] nb_sw_int = nb_server_interface.cable.b_terminations[0]
nb_sw = nb_sw_int.device nb_sw = nb_sw_int.device
nb_mgmt_int = nb.dcim.interfaces.get( nb_mgmt_int = nb.dcim.interfaces.get(device_id=nb_sw.id, mgmt_only=True)
device_id=nb_sw.id, nb_mgmt_ip = nb.ipam.ip_addresses.get(interface_id=nb_mgmt_int.id)
mgmt_only=True
)
nb_mgmt_ip = nb.ipam.ip_addresses.get(
interface_id=nb_mgmt_int.id
)
if nb_mgmt_ip is None: if nb_mgmt_ip is None:
logging.error( logging.error(
'Switch {switch_ip} does not have IP on its management interface'.format( "Switch {switch_ip} does not have IP on its management interface".format(
switch_ip=switch_ip, switch_ip=switch_ip,
) )
) )
return update, nb_server_interface return update, nb_server_interface
# Netbox IP is always IP/Netmask # Netbox IP is always IP/Netmask
nb_mgmt_ip = nb_mgmt_ip.address.split('/')[0] nb_mgmt_ip = nb_mgmt_ip.address.split("/")[0]
if nb_mgmt_ip != switch_ip or \ if nb_mgmt_ip != switch_ip or nb_sw_int.name != switch_interface:
nb_sw_int.name != switch_interface: logging.info("Netbox cable is not connected to correct ports, fixing..")
logging.info('Netbox cable is not connected to correct ports, fixing..')
logging.info( logging.info(
'Deleting cable {cable_id} from {interface} to {switch_interface} of ' "Deleting cable {cable_id} from {interface} to {switch_interface} of "
'{switch_ip}'.format( "{switch_ip}".format(
cable_id=nb_server_interface.cable.id, cable_id=nb_server_interface.cable.id,
interface=nb_server_interface.name, interface=nb_server_interface.name,
switch_interface=nb_sw_int.name, switch_interface=nb_sw_int.name,
switch_ip=nb_mgmt_ip, switch_ip=nb_mgmt_ip,
) )
) )
cable = nb.dcim.cables.get( cable = nb.dcim.cables.get(nb_server_interface.cable.id)
nb_server_interface.cable.id
)
cable.delete() cable.delete()
update = True update = True
nb_server_interface = self.connect_interface_to_switch( nb_server_interface = self.connect_interface_to_switch(
@ -629,17 +688,17 @@ class VirtualNetwork(Network):
self.server = server self.server = server
self.device = self.server.get_netbox_vm() self.device = self.server.get_netbox_vm()
self.nb_net = nb.virtualization self.nb_net = nb.virtualization
self.custom_arg = {'virtual_machine': getattr(self.device, "id", None)} self.custom_arg = {"virtual_machine": getattr(self.device, "id", None)}
self.custom_arg_id = {'virtual_machine_id': getattr(self.device, "id", None)} self.custom_arg_id = {"virtual_machine_id": getattr(self.device, "id", None)}
self.intf_type = "vminterface_id" self.intf_type = "vminterface_id"
self.assigned_object_type = "virtualization.vminterface" self.assigned_object_type = "virtualization.vminterface"
dcim_c = nb.virtualization.interfaces.choices() dcim_c = nb.virtualization.interfaces.choices()
for _choice_type in dcim_c: for _choice_type in dcim_c:
key = 'interface:{}'.format(_choice_type) key = "interface:{}".format(_choice_type)
self.dcim_choices[key] = {} self.dcim_choices[key] = {}
for choice in dcim_c[_choice_type]: for choice in dcim_c[_choice_type]:
self.dcim_choices[key][choice['display_name']] = choice['value'] self.dcim_choices[key][choice["display_name"]] = choice["value"]
def get_network_type(self): def get_network_type(self):
return 'virtual' return "virtual"

View file

@ -6,49 +6,51 @@ from netbox_agent.config import netbox_instance as nb
PSU_DMI_TYPE = 39 PSU_DMI_TYPE = 39
class PowerSupply(): class PowerSupply:
def __init__(self, server=None): def __init__(self, server=None):
self.server = server self.server = server
self.netbox_server = self.server.get_netbox_server() self.netbox_server = self.server.get_netbox_server()
if self.server.is_blade(): if self.server.is_blade():
self.device_id = self.netbox_server.parent_device.id if self.netbox_server else None self.device_id = (
self.netbox_server.parent_device.id if self.netbox_server else None
)
else: else:
self.device_id = self.netbox_server.id if self.netbox_server else None self.device_id = self.netbox_server.id if self.netbox_server else None
def get_power_supply(self): def get_power_supply(self):
power_supply = [] power_supply = []
for psu in dmidecode.get_by_type(self.server.dmi, PSU_DMI_TYPE): for psu in dmidecode.get_by_type(self.server.dmi, PSU_DMI_TYPE):
if 'Present' not in psu['Status'] or psu['Status'] == 'Not Present': if "Present" not in psu["Status"] or psu["Status"] == "Not Present":
continue continue
try: try:
max_power = int(psu.get('Max Power Capacity').split()[0]) max_power = int(psu.get("Max Power Capacity").split()[0])
except ValueError: except ValueError:
max_power = None max_power = None
desc = '{} - {}'.format( desc = "{} - {}".format(
psu.get('Manufacturer', 'No Manufacturer').strip(), psu.get("Manufacturer", "No Manufacturer").strip(),
psu.get('Name', 'No name').strip(), psu.get("Name", "No name").strip(),
) )
sn = psu.get('Serial Number', '').strip() sn = psu.get("Serial Number", "").strip()
# Let's assume that if no serial and no power reported we skip it # Let's assume that if no serial and no power reported we skip it
if sn == '' and max_power is None: if sn == "" and max_power is None:
continue continue
if sn == '': if sn == "":
sn = 'N/A' sn = "N/A"
power_supply.append({ power_supply.append(
'name': sn, {
'description': desc, "name": sn,
'allocated_draw': None, "description": desc,
'maximum_draw': max_power, "allocated_draw": None,
'device': self.device_id, "maximum_draw": max_power,
}) "device": self.device_id,
}
)
return power_supply return power_supply
def get_netbox_power_supply(self): def get_netbox_power_supply(self):
return nb.dcim.power_ports.filter( return nb.dcim.power_ports.filter(device_id=self.device_id)
device_id=self.device_id
)
def create_or_update_power_supply(self): def create_or_update_power_supply(self):
nb_psus = list(self.get_netbox_power_supply()) nb_psus = list(self.get_netbox_power_supply())
@ -57,10 +59,10 @@ class PowerSupply():
# Delete unknown PSU # Delete unknown PSU
delete = False delete = False
for nb_psu in nb_psus: for nb_psu in nb_psus:
if nb_psu.name not in [x['name'] for x in psus]: if nb_psu.name not in [x["name"] for x in psus]:
logging.info('Deleting unknown locally PSU {name}'.format( logging.info(
name=nb_psu.name "Deleting unknown locally PSU {name}".format(name=nb_psu.name)
)) )
nb_psu.delete() nb_psu.delete()
delete = True delete = True
@ -69,27 +71,23 @@ class PowerSupply():
# sync existing Netbox PSU with local infos # sync existing Netbox PSU with local infos
for nb_psu in nb_psus: for nb_psu in nb_psus:
local_psu = next( local_psu = next(item for item in psus if item["name"] == nb_psu.name)
item for item in psus if item['name'] == nb_psu.name
)
update = False update = False
if nb_psu.description != local_psu['description']: if nb_psu.description != local_psu["description"]:
update = True update = True
nb_psu.description = local_psu['description'] nb_psu.description = local_psu["description"]
if nb_psu.maximum_draw != local_psu['maximum_draw']: if nb_psu.maximum_draw != local_psu["maximum_draw"]:
update = True update = True
nb_psu.maximum_draw = local_psu['maximum_draw'] nb_psu.maximum_draw = local_psu["maximum_draw"]
if update: if update:
nb_psu.save() nb_psu.save()
for psu in psus: for psu in psus:
if psu['name'] not in [x.name for x in nb_psus]: if psu["name"] not in [x.name for x in nb_psus]:
logging.info('Creating PSU {name} ({description}), {maximum_draw}W'.format( logging.info(
**psu "Creating PSU {name} ({description}), {maximum_draw}W".format(**psu)
))
nb_psu = nb.dcim.power_ports.create(
**psu
) )
nb_psu = nb.dcim.power_ports.create(**psu)
return True return True
@ -97,7 +95,7 @@ class PowerSupply():
try: try:
psu_cons = self.server.get_power_consumption() psu_cons = self.server.get_power_consumption()
except NotImplementedError: except NotImplementedError:
logging.error('Cannot report power consumption for this vendor') logging.error("Cannot report power consumption for this vendor")
return False return False
nb_psus = self.get_netbox_power_supply() nb_psus = self.get_netbox_power_supply()
@ -107,25 +105,25 @@ class PowerSupply():
# find power feeds for rack or dc # find power feeds for rack or dc
pwr_feeds = None pwr_feeds = None
if self.netbox_server.rack: if self.netbox_server.rack:
pwr_feeds = nb.dcim.power_feeds.filter( pwr_feeds = nb.dcim.power_feeds.filter(rack=self.netbox_server.rack.id)
rack=self.netbox_server.rack.id
)
if pwr_feeds: if pwr_feeds:
voltage = [p['voltage'] for p in pwr_feeds] voltage = [p["voltage"] for p in pwr_feeds]
else: else:
logging.info('Could not find power feeds for Rack, defaulting value to 230') logging.info("Could not find power feeds for Rack, defaulting value to 230")
voltage = [230 for _ in nb_psus] voltage = [230 for _ in nb_psus]
for i, nb_psu in enumerate(nb_psus): for i, nb_psu in enumerate(nb_psus):
nb_psu.allocated_draw = int(float(psu_cons[i]) * voltage[i]) nb_psu.allocated_draw = int(float(psu_cons[i]) * voltage[i])
if nb_psu.allocated_draw < 1: if nb_psu.allocated_draw < 1:
logging.info('PSU is not connected or in standby mode') logging.info("PSU is not connected or in standby mode")
continue continue
nb_psu.save() nb_psu.save()
logging.info('Updated power consumption for PSU {}: {}W'.format( logging.info(
"Updated power consumption for PSU {}: {}W".format(
nb_psu.name, nb_psu.name,
nb_psu.allocated_draw, nb_psu.allocated_draw,
)) )
)
return True return True

View file

@ -1,4 +1,4 @@
class RaidController(): class RaidController:
def get_product_name(self): def get_product_name(self):
raise NotImplementedError raise NotImplementedError
@ -19,6 +19,6 @@ class RaidController():
return False return False
class Raid(): class Raid:
def get_controllers(self): def get_controllers(self):
raise NotImplementedError raise NotImplementedError

View file

@ -1,45 +1,50 @@
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 logging
import re import re
import subprocess
from netbox_agent.misc import get_vendor
from netbox_agent.raid.base import Raid, RaidController
REGEXP_CONTROLLER_HP = re.compile(r"Smart Array ([a-zA-Z0-9- ]+) in Slot ([0-9]+)")
REGEXP_CONTROLLER_HP = re.compile(r'Smart Array ([a-zA-Z0-9- ]+) in Slot ([0-9]+)')
class HPRaidControllerError(Exception): class HPRaidControllerError(Exception):
pass pass
def ssacli(sub_command): def ssacli(sub_command):
command = ["ssacli"] command = ["ssacli"]
command.extend(sub_command.split()) command.extend(sub_command.split())
p = subprocess.Popen( p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
stdout = stdout.decode("utf-8") stdout = stdout.decode("utf-8")
if p.returncode != 0: if p.returncode != 0:
mesg = "Failed to execute command '{}':\n{}".format( mesg = "Failed to execute command '{}':\n{}".format(" ".join(command), stdout)
" ".join(command), stdout
)
raise HPRaidControllerError(mesg) raise HPRaidControllerError(mesg)
if 'does not have any physical' in stdout: if "does not have any physical" in stdout:
return list() return list()
else: else:
lines = stdout.split('\n') lines = stdout.split("\n")
lines = list(filter(None, lines)) lines = list(filter(None, lines))
return lines return lines
def _test_if_valid_line(line): def _test_if_valid_line(line):
ignore_patterns = ['Note:', 'Error:', 'is not loaded', 'README', ' failure', ' cache'] ignore_patterns = [
"Note:",
"Error:",
"is not loaded",
"README",
" failure",
" cache",
]
for pattern in ignore_patterns: for pattern in ignore_patterns:
if not line or pattern in line: if not line or pattern in line:
return None return None
return line return line
def _parse_ctrl_output(lines): def _parse_ctrl_output(lines):
controllers = {} controllers = {}
current_ctrl = None current_ctrl = None
@ -53,14 +58,14 @@ def _parse_ctrl_output(lines):
if ctrl is not None: if ctrl is not None:
slot = ctrl.group(2) slot = ctrl.group(2)
current_ctrl = "{} - Slot {}".format(ctrl.group(1), slot) current_ctrl = "{} - Slot {}".format(ctrl.group(1), slot)
controllers[current_ctrl] = {'Slot': slot} controllers[current_ctrl] = {"Slot": slot}
if 'Embedded' not in line: if "Embedded" not in line:
controllers[current_ctrl]['External'] = True controllers[current_ctrl]["External"] = True
continue continue
if ': ' not in line: if ": " not in line:
continue continue
attr, val = line.split(': ', 1) attr, val = line.split(": ", 1)
attr = attr.strip() attr = attr.strip()
val = val.strip() val = val.strip()
controllers[current_ctrl][attr] = val controllers[current_ctrl][attr] = val
@ -78,25 +83,24 @@ def _parse_pd_output(lines):
if line is None: if line is None:
continue continue
# Parses the Array the drives are in # Parses the Array the drives are in
if line.startswith('Array'): if line.startswith("Array"):
current_array = line.split(None, 1)[1] current_array = line.split(None, 1)[1]
# Detects new physical drive # Detects new physical drive
if line.startswith('physicaldrive'): if line.startswith("physicaldrive"):
current_drv = line.split(None, 1)[1] current_drv = line.split(None, 1)[1]
drives[current_drv] = {} drives[current_drv] = {}
if current_array is not None: if current_array is not None:
drives[current_drv]['Array'] = current_array drives[current_drv]["Array"] = current_array
continue continue
if ': ' not in line: if ": " not in line:
continue continue
attr, val = line.split(': ', 1) attr, val = line.split(": ", 1)
attr = attr.strip() attr = attr.strip()
val = val.strip() val = val.strip()
drives.setdefault(current_drv, {})[attr] = val drives.setdefault(current_drv, {})[attr] = val
return drives return drives
def _parse_ld_output(lines): def _parse_ld_output(lines):
drives = {} drives = {}
current_array = None current_array = None
@ -108,17 +112,17 @@ def _parse_ld_output(lines):
if line is None: if line is None:
continue continue
# Parses the Array the drives are in # Parses the Array the drives are in
if line.startswith('Array'): if line.startswith("Array"):
current_array = line.split(None, 1)[1] current_array = line.split(None, 1)[1]
drives[current_array] = {} drives[current_array] = {}
# Detects new physical drive # Detects new physical drive
if line.startswith('Logical Drive'): if line.startswith("Logical Drive"):
current_drv = line.split(': ', 1)[1] current_drv = line.split(": ", 1)[1]
drives.setdefault(current_array, {})['LogicalDrive'] = current_drv drives.setdefault(current_array, {})["LogicalDrive"] = current_drv
continue continue
if ': ' not in line: if ": " not in line:
continue continue
attr, val = line.split(': ', 1) attr, val = line.split(": ", 1)
drives.setdefault(current_array, {})[attr] = val drives.setdefault(current_array, {})[attr] = val
return drives return drives
@ -128,7 +132,7 @@ class HPRaidController(RaidController):
self.controller_name = controller_name self.controller_name = controller_name
self.data = data self.data = data
self.pdrives = self._get_physical_disks() self.pdrives = self._get_physical_disks()
arrays = [d['Array'] for d in self.pdrives.values() if d.get('Array')] arrays = [d["Array"] for d in self.pdrives.values() if d.get("Array")]
if arrays: if arrays:
self.ldrives = self._get_logical_drives() self.ldrives = self._get_logical_drives()
self._get_virtual_drives_map() self._get_virtual_drives_map()
@ -137,64 +141,69 @@ class HPRaidController(RaidController):
return self.controller_name return self.controller_name
def get_manufacturer(self): def get_manufacturer(self):
return 'HP' return "HP"
def get_serial_number(self): def get_serial_number(self):
return self.data['Serial Number'] return self.data["Serial Number"]
def get_firmware_version(self): def get_firmware_version(self):
return self.data['Firmware Version'] return self.data["Firmware Version"]
def is_external(self): def is_external(self):
return self.data.get('External', False) return self.data.get("External", False)
def _get_physical_disks(self): def _get_physical_disks(self):
lines = ssacli('ctrl slot={} pd all show detail'.format(self.data['Slot'])) lines = ssacli("ctrl slot={} pd all show detail".format(self.data["Slot"]))
pdrives = _parse_pd_output(lines) pdrives = _parse_pd_output(lines)
ret = {} ret = {}
for name, attrs in pdrives.items(): for name, attrs in pdrives.items():
array = attrs.get('Array', '') array = attrs.get("Array", "")
model = attrs.get('Model', '').strip() model = attrs.get("Model", "").strip()
vendor = None vendor = None
if model.startswith('HP'): if model.startswith("HP"):
vendor = 'HP' vendor = "HP"
elif len(model.split()) > 1: elif len(model.split()) > 1:
vendor = get_vendor(model.split()[1]) vendor = get_vendor(model.split()[1])
else: else:
vendor = get_vendor(model) vendor = get_vendor(model)
ret[name] = { ret[name] = {
'Array': array, "Array": array,
'Model': model, "Model": model,
'Vendor': vendor, "Vendor": vendor,
'SN': attrs.get('Serial Number', '').strip(), "SN": attrs.get("Serial Number", "").strip(),
'Size': attrs.get('Size', '').strip(), "Size": attrs.get("Size", "").strip(),
'Type': 'SSD' if attrs.get('Interface Type') == 'Solid State SATA' "Type": (
else 'HDD', "SSD"
'_src': self.__class__.__name__, if attrs.get("Interface Type") == "Solid State SATA"
'custom_fields': { else "HDD"
'pd_identifier': name, ),
'mount_point': attrs.get('Mount Points', '').strip(), "_src": self.__class__.__name__,
'vd_device': attrs.get('Disk Name', '').strip(), "custom_fields": {
'vd_size': attrs.get('Size', '').strip(), "pd_identifier": name,
} "mount_point": attrs.get("Mount Points", "").strip(),
"vd_device": attrs.get("Disk Name", "").strip(),
"vd_size": attrs.get("Size", "").strip(),
},
} }
return ret return ret
def _get_logical_drives(self): def _get_logical_drives(self):
lines = ssacli('ctrl slot={} ld all show detail'.format(self.data['Slot'])) lines = ssacli("ctrl slot={} ld all show detail".format(self.data["Slot"]))
ldrives = _parse_ld_output(lines) ldrives = _parse_ld_output(lines)
ret = {} ret = {}
for array, attrs in ldrives.items(): for array, attrs in ldrives.items():
ret[array] = { ret[array] = {
'vd_array': array, "vd_array": array,
'vd_size': attrs.get('Size', '').strip(), "vd_size": attrs.get("Size", "").strip(),
'vd_consistency': attrs.get('Status', '').strip(), "vd_consistency": attrs.get("Status", "").strip(),
'vd_raid_type': 'RAID {}'.format(attrs.get('Fault Tolerance', 'N/A').strip()), "vd_raid_type": "RAID {}".format(
'vd_device': attrs.get('LogicalDrive', '').strip(), attrs.get("Fault Tolerance", "N/A").strip()
'mount_point': attrs.get('Mount Points', '').strip() ),
"vd_device": attrs.get("LogicalDrive", "").strip(),
"mount_point": attrs.get("Mount Points", "").strip(),
} }
return ret return ret
@ -208,7 +217,7 @@ class HPRaidController(RaidController):
" Ignoring.".format(name) " Ignoring.".format(name)
) )
continue continue
attrs['custom_fields'].update(ld) attrs["custom_fields"].update(ld)
def get_physical_disks(self): def get_physical_disks(self):
return list(self.pdrives.values()) return list(self.pdrives.values())
@ -216,18 +225,16 @@ class HPRaidController(RaidController):
class HPRaid(Raid): class HPRaid(Raid):
def __init__(self): def __init__(self):
self.output = subprocess.getoutput('ssacli ctrl all show detail') self.output = subprocess.getoutput("ssacli ctrl all show detail")
self.controllers = [] self.controllers = []
self.convert_to_dict() self.convert_to_dict()
def convert_to_dict(self): def convert_to_dict(self):
lines = self.output.split('\n') lines = self.output.split("\n")
lines = list(filter(None, lines)) lines = list(filter(None, lines))
controllers = _parse_ctrl_output(lines) controllers = _parse_ctrl_output(lines)
for controller, attrs in controllers.items(): for controller, attrs in controllers.items():
self.controllers.append( self.controllers.append(HPRaidController(controller, attrs))
HPRaidController(controller, attrs)
)
def get_controllers(self): def get_controllers(self):
return self.controllers return self.controllers

View file

@ -1,9 +1,9 @@
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 logging
import re import re
import subprocess
from netbox_agent.misc import get_mount_points, get_vendor
from netbox_agent.raid.base import Raid, RaidController
class OmreportControllerError(Exception): class OmreportControllerError(Exception):
@ -13,30 +13,24 @@ class OmreportControllerError(Exception):
def omreport(sub_command): def omreport(sub_command):
command = ["omreport"] command = ["omreport"]
command.extend(sub_command.split()) command.extend(sub_command.split())
p = subprocess.Popen( p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
p.wait() p.wait()
stdout = p.stdout.read().decode("utf-8") stdout = p.stdout.read().decode("utf-8")
if p.returncode != 0: if p.returncode != 0:
mesg = "Failed to execute command '{}':\n{}".format( mesg = "Failed to execute command '{}':\n{}".format(" ".join(command), stdout)
" ".join(command), stdout
)
raise OmreportControllerError(mesg) raise OmreportControllerError(mesg)
res = {} res = {}
section_re = re.compile('^[A-Z]') section_re = re.compile("^[A-Z]")
current_section = None current_section = None
current_obj = None current_obj = None
for line in stdout.split('\n'): for line in stdout.split("\n"):
if ': ' in line: if ": " in line:
attr, value = line.split(': ', 1) attr, value = line.split(": ", 1)
attr = attr.strip() attr = attr.strip()
value = value.strip() value = value.strip()
if attr == 'ID': if attr == "ID":
obj = {} obj = {}
res.setdefault(current_section, []).append(obj) res.setdefault(current_section, []).append(obj)
current_obj = obj current_obj = obj
@ -52,60 +46,57 @@ class OmreportController(RaidController):
self.controller_index = controller_index self.controller_index = controller_index
def get_product_name(self): def get_product_name(self):
return self.data['Name'] return self.data["Name"]
def get_manufacturer(self): def get_manufacturer(self):
return None return None
def get_serial_number(self): def get_serial_number(self):
return self.data.get('DeviceSerialNumber') return self.data.get("DeviceSerialNumber")
def get_firmware_version(self): def get_firmware_version(self):
return self.data.get('Firmware Version') return self.data.get("Firmware Version")
def _get_physical_disks(self): def _get_physical_disks(self):
pds = {} pds = {}
res = omreport('storage pdisk controller={}'.format( res = omreport("storage pdisk controller={}".format(self.controller_index))
self.controller_index
))
for pdisk in [d for d in list(res.values())[0]]: for pdisk in [d for d in list(res.values())[0]]:
disk_id = pdisk['ID'] disk_id = pdisk["ID"]
size = re.sub('B .*$', 'B', pdisk['Capacity']) size = re.sub("B .*$", "B", pdisk["Capacity"])
pds[disk_id] = { pds[disk_id] = {
'Vendor': get_vendor(pdisk['Vendor ID']), "Vendor": get_vendor(pdisk["Vendor ID"]),
'Model': pdisk['Product ID'], "Model": pdisk["Product ID"],
'SN': pdisk['Serial No.'], "SN": pdisk["Serial No."],
'Size': size, "Size": size,
'Type': pdisk['Media'], "Type": pdisk["Media"],
'_src': self.__class__.__name__, "_src": self.__class__.__name__,
} }
return pds return pds
def _get_virtual_drives_map(self): def _get_virtual_drives_map(self):
pds = {} pds = {}
res = omreport('storage vdisk controller={}'.format( res = omreport("storage vdisk controller={}".format(self.controller_index))
self.controller_index
))
for vdisk in [d for d in list(res.values())[0]]: for vdisk in [d for d in list(res.values())[0]]:
vdisk_id = vdisk['ID'] vdisk_id = vdisk["ID"]
device = vdisk['Device Name'] device = vdisk["Device Name"]
mount_points = get_mount_points() mount_points = get_mount_points()
mp = mount_points.get(device, 'n/a') mp = mount_points.get(device, "n/a")
size = re.sub('B .*$', 'B', vdisk['Size']) size = re.sub("B .*$", "B", vdisk["Size"])
vd = { vd = {
'vd_array': vdisk_id, "vd_array": vdisk_id,
'vd_size': size, "vd_size": size,
'vd_consistency': vdisk['State'], "vd_consistency": vdisk["State"],
'vd_raid_type': vdisk['Layout'], "vd_raid_type": vdisk["Layout"],
'vd_device': vdisk['Device Name'], "vd_device": vdisk["Device Name"],
'mount_point': ', '.join(sorted(mp)), "mount_point": ", ".join(sorted(mp)),
} }
drives_res = omreport( drives_res = omreport(
'storage pdisk controller={} vdisk={}'.format( "storage pdisk controller={} vdisk={}".format(
self.controller_index, vdisk_id self.controller_index, vdisk_id
)) )
)
for pdisk in [d for d in list(drives_res.values())[0]]: for pdisk in [d for d in list(drives_res.values())[0]]:
pds[pdisk['ID']] = vd pds[pdisk["ID"]] = vd
return pds return pds
def get_physical_disks(self): def get_physical_disks(self):
@ -114,27 +105,23 @@ class OmreportController(RaidController):
for pd_identifier, vd in vds.items(): for pd_identifier, vd in vds.items():
if pd_identifier not in pds: if pd_identifier not in pds:
logging.error( logging.error(
'Physical drive {} listed in virtual drive {} not ' "Physical drive {} listed in virtual drive {} not "
'found in drives list'.format( "found in drives list".format(pd_identifier, vd["vd_array"])
pd_identifier, vd['vd_array']
)
) )
continue continue
pds[pd_identifier].setdefault('custom_fields', {}).update(vd) pds[pd_identifier].setdefault("custom_fields", {}).update(vd)
pds[pd_identifier]['custom_fields']['pd_identifier'] = pd_identifier pds[pd_identifier]["custom_fields"]["pd_identifier"] = pd_identifier
return list(pds.values()) return list(pds.values())
class OmreportRaid(Raid): class OmreportRaid(Raid):
def __init__(self): def __init__(self):
self.controllers = [] self.controllers = []
res = omreport('storage controller') res = omreport("storage controller")
for controller in res['Controller']: for controller in res["Controller"]:
ctrl_index = controller['ID'] ctrl_index = controller["ID"]
self.controllers.append( self.controllers.append(OmreportController(ctrl_index, controller))
OmreportController(ctrl_index, controller)
)
def get_controllers(self): def get_controllers(self):
return self.controllers return self.controllers

View file

@ -1,11 +1,12 @@
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 json
import re import logging
import os import os
import re
import subprocess
from netbox_agent.config import config
from netbox_agent.misc import get_mount_points, get_vendor
from netbox_agent.raid.base import Raid, RaidController
class StorcliControllerError(Exception): class StorcliControllerError(Exception):
@ -16,29 +17,23 @@ def storecli(sub_command):
command = ["storcli"] command = ["storcli"]
command.extend(sub_command.split()) command.extend(sub_command.split())
command.append("J") command.append("J")
p = subprocess.Popen( p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
if stderr: if stderr:
mesg = "Failed to execute command '{}':\n{}".format( mesg = "Failed to execute command '{}':\n{}".format(" ".join(command), stdout)
" ".join(command), stdout
)
raise StorcliControllerError(mesg) raise StorcliControllerError(mesg)
stdout = stdout.decode("utf-8") stdout = stdout.decode("utf-8")
data = json.loads(stdout) data = json.loads(stdout)
controllers = dict([ controllers = dict(
( [
c['Command Status']['Controller'], (c["Command Status"]["Controller"], c["Response Data"])
c['Response Data'] for c in data["Controllers"]
) for c in data['Controllers'] if c["Command Status"]["Status"] == "Success"
if c['Command Status']['Status'] == 'Success' ]
]) )
if not controllers: if not controllers:
logging.error( logging.error(
"Failed to execute command '{}'. " "Failed to execute command '{}'. "
@ -54,23 +49,23 @@ class StorcliController(RaidController):
self.controller_index = controller_index self.controller_index = controller_index
def get_product_name(self): def get_product_name(self):
return self.data['Product Name'] return self.data["Product Name"]
def get_manufacturer(self): def get_manufacturer(self):
return None return None
def get_serial_number(self): def get_serial_number(self):
return self.data['Serial Number'] return self.data["Serial Number"]
def get_firmware_version(self): def get_firmware_version(self):
return self.data['FW Package Build'] return self.data["FW Package Build"]
def _get_physical_disks(self): def _get_physical_disks(self):
pds = {} pds = {}
cmd = '/c{}/eall/sall show all'.format(self.controller_index) cmd = "/c{}/eall/sall show all".format(self.controller_index)
controllers = storecli(cmd) controllers = storecli(cmd)
pd_info = controllers[self.controller_index] pd_info = controllers[self.controller_index]
pd_re = re.compile(r'^Drive (/c\d+/e\d+/s\d+)$') pd_re = re.compile(r"^Drive (/c\d+/e\d+/s\d+)$")
for section, attrs in pd_info.items(): for section, attrs in pd_info.items():
reg = pd_re.search(section) reg = pd_re.search(section)
@ -78,28 +73,28 @@ class StorcliController(RaidController):
continue continue
pd_name = reg.group(1) pd_name = reg.group(1)
pd_attr = attrs[0] pd_attr = attrs[0]
pd_identifier = pd_attr['EID:Slt'] pd_identifier = pd_attr["EID:Slt"]
size = pd_attr.get('Size', '').strip() size = pd_attr.get("Size", "").strip()
media_type = pd_attr.get('Med', '').strip() media_type = pd_attr.get("Med", "").strip()
pd_details = pd_info['{} - Detailed Information'.format(section)] pd_details = pd_info["{} - Detailed Information".format(section)]
pd_dev_attr = pd_details['{} Device attributes'.format(section)] pd_dev_attr = pd_details["{} Device attributes".format(section)]
model = pd_dev_attr.get('Model Number', '').strip() model = pd_dev_attr.get("Model Number", "").strip()
pd = { pd = {
'Model': model, "Model": model,
'Vendor': get_vendor(model), "Vendor": get_vendor(model),
'SN': pd_dev_attr.get('SN', '').strip(), "SN": pd_dev_attr.get("SN", "").strip(),
'Size': size, "Size": size,
'Type': media_type, "Type": media_type,
'_src': self.__class__.__name__, "_src": self.__class__.__name__,
} }
if config.process_virtual_drives: if config.process_virtual_drives:
pd.setdefault('custom_fields', {})['pd_identifier'] = pd_name pd.setdefault("custom_fields", {})["pd_identifier"] = pd_name
pds[pd_identifier] = pd pds[pd_identifier] = pd
return pds return pds
def _get_virtual_drives_map(self): def _get_virtual_drives_map(self):
vds = {} vds = {}
cmd = '/c{}/vall show all'.format(self.controller_index) cmd = "/c{}/vall show all".format(self.controller_index)
controllers = storecli(cmd) controllers = storecli(cmd)
vd_info = controllers[self.controller_index] vd_info = controllers[self.controller_index]
mount_points = get_mount_points() mount_points = get_mount_points()
@ -109,9 +104,9 @@ class StorcliController(RaidController):
continue continue
volume = vd_identifier.split("/")[-1].lstrip("v") volume = vd_identifier.split("/")[-1].lstrip("v")
vd_attr = vd_attrs[0] vd_attr = vd_attrs[0]
vd_pd_identifier = 'PDs for VD {}'.format(volume) vd_pd_identifier = "PDs for VD {}".format(volume)
vd_pds = vd_info[vd_pd_identifier] vd_pds = vd_info[vd_pd_identifier]
vd_prop_identifier = 'VD{} Properties'.format(volume) vd_prop_identifier = "VD{} Properties".format(volume)
vd_properties = vd_info[vd_prop_identifier] vd_properties = vd_info[vd_prop_identifier]
for pd in vd_pds: for pd in vd_pds:
pd_identifier = pd["EID:Slt"] pd_identifier = pd["EID:Slt"]
@ -125,7 +120,7 @@ class StorcliController(RaidController):
"vd_consistency": vd_attr["Consist"], "vd_consistency": vd_attr["Consist"],
"vd_raid_type": vd_attr["TYPE"], "vd_raid_type": vd_attr["TYPE"],
"vd_device": device, "vd_device": device,
"mount_point": ", ".join(sorted(mp)) "mount_point": ", ".join(sorted(mp)),
} }
return vds return vds
@ -139,9 +134,7 @@ class StorcliController(RaidController):
if pd_identifier not in pds: if pd_identifier not in pds:
logging.error( logging.error(
"Physical drive {} listed in virtual drive {} not " "Physical drive {} listed in virtual drive {} not "
"found in drives list".format( "found in drives list".format(pd_identifier, vd["vd_array"])
pd_identifier, vd["vd_array"]
)
) )
continue continue
pds[pd_identifier].setdefault("custom_fields", {}).update(vd) pds[pd_identifier].setdefault("custom_fields", {}).update(vd)
@ -152,14 +145,9 @@ class StorcliController(RaidController):
class StorcliRaid(Raid): class StorcliRaid(Raid):
def __init__(self): def __init__(self):
self.controllers = [] self.controllers = []
controllers = storecli('/call show') controllers = storecli("/call show")
for controller_id, controller_data in controllers.items(): for controller_id, controller_data in controllers.items():
self.controllers.append( self.controllers.append(StorcliController(controller_id, controller_data))
StorcliController(
controller_id,
controller_data
)
)
def get_controllers(self): def get_controllers(self):
return self.controllers return self.controllers

View file

@ -1,46 +1,57 @@
import logging
import socket
import subprocess
import sys
from pprint import pprint
import netbox_agent.dmidecode as dmidecode import netbox_agent.dmidecode as dmidecode
from netbox_agent.config import config from netbox_agent.config import config
from netbox_agent.config import netbox_instance as nb from netbox_agent.config import netbox_instance as nb
from netbox_agent.inventory import Inventory from netbox_agent.inventory import Inventory
from netbox_agent.location import Datacenter, Rack, Tenant from netbox_agent.location import Datacenter, Rack, Tenant
from netbox_agent.misc import create_netbox_tags, get_device_role, get_device_type, get_device_platform 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.network import ServerNetwork
from netbox_agent.power import PowerSupply from netbox_agent.power import PowerSupply
from pprint import pprint
import subprocess
import logging
import socket
import sys
class ServerBase(): class ServerBase:
def __init__(self, dmi=None): def __init__(self, dmi=None):
if dmi: if dmi:
self.dmi = dmi self.dmi = dmi
else: else:
self.dmi = dmidecode.parse() self.dmi = dmidecode.parse()
self.baseboard = dmidecode.get_by_type(self.dmi, 'Baseboard') self.baseboard = dmidecode.get_by_type(self.dmi, "Baseboard")
self.bios = dmidecode.get_by_type(self.dmi, 'BIOS') self.bios = dmidecode.get_by_type(self.dmi, "BIOS")
self.chassis = dmidecode.get_by_type(self.dmi, 'Chassis') self.chassis = dmidecode.get_by_type(self.dmi, "Chassis")
self.system = dmidecode.get_by_type(self.dmi, 'System') self.system = dmidecode.get_by_type(self.dmi, "System")
self.device_platform = get_device_platform(config.device.platform) self.device_platform = get_device_platform(config.device.platform)
self.network = None self.network = None
self.tags = list(set([ self.tags = (
x.strip() for x in config.device.tags.split(',') if x.strip() list(set([x.strip() for x in config.device.tags.split(",") if x.strip()]))
])) if config.device.tags else [] if config.device.tags
else []
)
self.nb_tags = list(create_netbox_tags(self.tags)) self.nb_tags = list(create_netbox_tags(self.tags))
config_cf = set([ config_cf = set(
f.strip() for f in config.device.custom_fields.split(",") [f.strip() for f in config.device.custom_fields.split(",") if f.strip()]
if f.strip() )
])
self.custom_fields = {} self.custom_fields = {}
self.custom_fields.update(dict([ self.custom_fields.update(
(k.strip(), v.strip()) for k, v in dict(
[f.split("=", 1) for f in config_cf] [
])) (k.strip(), v.strip())
for k, v in [f.split("=", 1) for f in config_cf]
]
)
)
def get_tenant(self): def get_tenant(self):
tenant = Tenant() tenant = Tenant()
@ -50,9 +61,7 @@ class ServerBase():
tenant = self.get_tenant() tenant = self.get_tenant()
if tenant is None: if tenant is None:
return None return None
nb_tenant = nb.tenancy.tenants.get( nb_tenant = nb.tenancy.tenants.get(slug=self.get_tenant())
slug=self.get_tenant()
)
return nb_tenant return nb_tenant
def get_datacenter(self): def get_datacenter(self):
@ -81,22 +90,22 @@ class ServerBase():
update = False update = False
if dc and server.site and server.site.slug != nb_dc.slug: if dc and server.site and server.site.slug != nb_dc.slug:
logging.info('Datacenter location has changed from {} to {}, updating'.format( logging.info(
"Datacenter location has changed from {} to {}, updating".format(
server.site.slug, server.site.slug,
nb_dc.slug, nb_dc.slug,
)) )
)
update = True update = True
server.site = nb_dc.id server.site = nb_dc.id
if ( if server.rack and nb_rack and server.rack.id != nb_rack.id:
server.rack logging.info(
and nb_rack "Rack location has changed from {} to {}, updating".format(
and server.rack.id != nb_rack.id
):
logging.info('Rack location has changed from {} to {}, updating'.format(
server.rack, server.rack,
nb_rack, nb_rack,
)) )
)
update = True update = True
server.rack = nb_rack server.rack = nb_rack
if nb_rack is None: if nb_rack is None:
@ -139,24 +148,24 @@ class ServerBase():
""" """
Return the Chassis Name from dmidecode info Return the Chassis Name from dmidecode info
""" """
return self.system[0]['Product Name'].strip() return self.system[0]["Product Name"].strip()
def get_service_tag(self): def get_service_tag(self):
""" """
Return the Service Tag from dmidecode info Return the Service Tag from dmidecode info
""" """
return self.system[0]['Serial Number'].strip() return self.system[0]["Serial Number"].strip()
def get_expansion_service_tag(self): def get_expansion_service_tag(self):
""" """
Return the virtual Service Tag from dmidecode info host Return the virtual Service Tag from dmidecode info host
with 'expansion' with 'expansion'
""" """
return self.system[0]['Serial Number'].strip() + " expansion" return self.system[0]["Serial Number"].strip() + " expansion"
def get_hostname(self): def get_hostname(self):
if config.hostname_cmd is None: if config.hostname_cmd is None:
return '{}'.format(socket.gethostname()) return "{}".format(socket.gethostname())
return subprocess.getoutput(config.hostname_cmd) return subprocess.getoutput(config.hostname_cmd)
def is_blade(self): def is_blade(self):
@ -193,8 +202,7 @@ class ServerBase():
device_type = get_device_type(self.get_chassis()) device_type = get_device_type(self.get_chassis())
device_role = get_device_role(config.device.chassis_role) device_role = get_device_role(config.device.chassis_role)
serial = self.get_chassis_service_tag() serial = self.get_chassis_service_tag()
logging.info('Creating chassis blade (serial: {serial})'.format( logging.info("Creating chassis blade (serial: {serial})".format(serial=serial))
serial=serial))
new_chassis = nb.dcim.devices.create( new_chassis = nb.dcim.devices.create(
name=self.get_chassis_name(), name=self.get_chassis_name(),
device_type=device_type.id, device_type=device_type.id,
@ -203,7 +211,7 @@ class ServerBase():
site=datacenter.id if datacenter else None, site=datacenter.id if datacenter else None,
tenant=tenant.id if tenant else None, tenant=tenant.id if tenant else None,
rack=rack.id if rack else None, rack=rack.id if rack else None,
tags=[{'name': x} for x in self.tags], tags=[{"name": x} for x in self.tags],
custom_fields=self.custom_fields, custom_fields=self.custom_fields,
) )
return new_chassis return new_chassis
@ -214,9 +222,10 @@ class ServerBase():
serial = self.get_service_tag() serial = self.get_service_tag()
hostname = self.get_hostname() hostname = self.get_hostname()
logging.info( logging.info(
'Creating blade (serial: {serial}) {hostname} on chassis {chassis_serial}'.format( "Creating blade (serial: {serial}) {hostname} on chassis {chassis_serial}".format(
serial=serial, hostname=hostname, chassis_serial=chassis.serial serial=serial, hostname=hostname, chassis_serial=chassis.serial
)) )
)
new_blade = nb.dcim.devices.create( new_blade = nb.dcim.devices.create(
name=hostname, name=hostname,
serial=serial, serial=serial,
@ -226,7 +235,7 @@ class ServerBase():
site=datacenter.id if datacenter else None, site=datacenter.id if datacenter else None,
tenant=tenant.id if tenant else None, tenant=tenant.id if tenant else None,
rack=rack.id if rack else None, rack=rack.id if rack else None,
tags=[{'name': x} for x in self.tags], tags=[{"name": x} for x in self.tags],
custom_fields=self.custom_fields, custom_fields=self.custom_fields,
) )
return new_blade return new_blade
@ -237,9 +246,10 @@ class ServerBase():
serial = self.get_expansion_service_tag() serial = self.get_expansion_service_tag()
hostname = self.get_hostname() + " expansion" hostname = self.get_hostname() + " expansion"
logging.info( logging.info(
'Creating expansion (serial: {serial}) {hostname} on chassis {chassis_serial}'.format( "Creating expansion (serial: {serial}) {hostname} on chassis {chassis_serial}".format(
serial=serial, hostname=hostname, chassis_serial=chassis.serial serial=serial, hostname=hostname, chassis_serial=chassis.serial
)) )
)
new_blade = nb.dcim.devices.create( new_blade = nb.dcim.devices.create(
name=hostname, name=hostname,
serial=serial, serial=serial,
@ -249,7 +259,7 @@ class ServerBase():
site=datacenter.id if datacenter else None, site=datacenter.id if datacenter else None,
tenant=tenant.id if tenant else None, tenant=tenant.id if tenant else None,
rack=rack.id if rack else None, rack=rack.id if rack else None,
tags=[{'name': x} for x in self.tags], tags=[{"name": x} for x in self.tags],
) )
return new_blade return new_blade
@ -267,8 +277,11 @@ class ServerBase():
raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis())) raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis()))
serial = self.get_service_tag() serial = self.get_service_tag()
hostname = self.get_hostname() hostname = self.get_hostname()
logging.info('Creating server (serial: {serial}) {hostname}'.format( logging.info(
serial=serial, hostname=hostname)) "Creating server (serial: {serial}) {hostname}".format(
serial=serial, hostname=hostname
)
)
new_server = nb.dcim.devices.create( new_server = nb.dcim.devices.create(
name=hostname, name=hostname,
serial=serial, serial=serial,
@ -278,7 +291,7 @@ class ServerBase():
site=datacenter.id if datacenter else None, site=datacenter.id if datacenter else None,
tenant=tenant.id if tenant else None, tenant=tenant.id if tenant else None,
rack=rack.id if rack else None, rack=rack.id if rack else None,
tags=[{'name': x} for x in self.tags], tags=[{"name": x} for x in self.tags],
) )
return new_server return new_server
@ -290,14 +303,16 @@ class ServerBase():
def _netbox_set_or_update_blade_slot(self, server, chassis, datacenter): def _netbox_set_or_update_blade_slot(self, server, chassis, datacenter):
# before everything check if right chassis # before everything check if right chassis
actual_device_bay = server.parent_device.device_bay \ actual_device_bay = (
if server.parent_device else None server.parent_device.device_bay if server.parent_device else None
actual_chassis = actual_device_bay.device \ )
if actual_device_bay else None actual_chassis = actual_device_bay.device if actual_device_bay else None
slot = self.get_blade_slot() slot = self.get_blade_slot()
if actual_chassis and \ if (
actual_chassis.serial == chassis.serial and \ actual_chassis
actual_device_bay.name == slot: and actual_chassis.serial == chassis.serial
and actual_device_bay.name == slot
):
return return
real_device_bays = nb.dcim.device_bays.filter( real_device_bays = nb.dcim.device_bays.filter(
@ -310,10 +325,11 @@ class ServerBase():
) )
if real_device_bays: if real_device_bays:
logging.info( logging.info(
'Setting device ({serial}) new slot on {slot} ' "Setting device ({serial}) new slot on {slot} "
'(Chassis {chassis_serial})..'.format( "(Chassis {chassis_serial})..".format(
serial=server.serial, slot=slot, chassis_serial=chassis.serial serial=server.serial, slot=slot, chassis_serial=chassis.serial
)) )
)
# reset actual device bay if set # reset actual device bay if set
if actual_device_bay: if actual_device_bay:
# Forces the evaluation of the installed_device attribute to # Forces the evaluation of the installed_device attribute to
@ -327,18 +343,22 @@ class ServerBase():
real_device_bay.installed_device = server real_device_bay.installed_device = server
real_device_bay.save() real_device_bay.save()
else: else:
logging.error('Could not find slot {slot} for chassis'.format( logging.error("Could not find slot {slot} for chassis".format(slot=slot))
slot=slot
))
def _netbox_set_or_update_blade_expansion_slot(self, expansion, chassis, datacenter): def _netbox_set_or_update_blade_expansion_slot(
self, expansion, chassis, datacenter
):
# before everything check if right chassis # before everything check if right chassis
actual_device_bay = expansion.parent_device.device_bay if expansion.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 actual_chassis = actual_device_bay.device if actual_device_bay else None
slot = self.get_blade_expansion_slot() slot = self.get_blade_expansion_slot()
if actual_chassis and \ if (
actual_chassis.serial == chassis.serial and \ actual_chassis
actual_device_bay.name == slot: and actual_chassis.serial == chassis.serial
and actual_device_bay.name == slot
):
return return
real_device_bays = nb.dcim.device_bays.filter( real_device_bays = nb.dcim.device_bays.filter(
@ -346,15 +366,16 @@ class ServerBase():
name=slot, name=slot,
) )
if not real_device_bays: if not real_device_bays:
logging.error('Could not find slot {slot} expansion for chassis'.format( logging.error(
slot=slot "Could not find slot {slot} expansion for chassis".format(slot=slot)
)) )
return return
logging.info( logging.info(
'Setting device expansion ({serial}) new slot on {slot} ' "Setting device expansion ({serial}) new slot on {slot} "
'(Chassis {chassis_serial})..'.format( "(Chassis {chassis_serial})..".format(
serial=expansion.serial, slot=slot, chassis_serial=chassis.serial serial=expansion.serial, slot=slot, chassis_serial=chassis.serial
)) )
)
# reset actual device bay if set # reset actual device bay if set
if actual_device_bay: if actual_device_bay:
# Forces the evaluation of the installed_device attribute to # Forces the evaluation of the installed_device attribute to
@ -388,9 +409,7 @@ class ServerBase():
self._netbox_deduplicate_server() self._netbox_deduplicate_server()
if self.is_blade(): if self.is_blade():
chassis = nb.dcim.devices.get( chassis = nb.dcim.devices.get(serial=self.get_chassis_service_tag())
serial=self.get_chassis_service_tag()
)
# Chassis does not exist # Chassis does not exist
if not chassis: if not chassis:
chassis = self._netbox_create_chassis(datacenter, tenant, rack) chassis = self._netbox_create_chassis(datacenter, tenant, rack)
@ -406,13 +425,14 @@ class ServerBase():
if not server: if not server:
server = self._netbox_create_server(datacenter, tenant, rack) server = self._netbox_create_server(datacenter, tenant, rack)
logging.debug('Updating Server...') logging.debug("Updating Server...")
# check network cards # check network cards
if config.register or config.update_all or config.update_network: if config.register or config.update_all or config.update_network:
self.network = ServerNetwork(server=self) self.network = ServerNetwork(server=self)
self.network.create_or_update_netbox_network_cards() self.network.create_or_update_netbox_network_cards()
update_inventory = config.inventory and (config.register or update_inventory = config.inventory and (
config.update_all or config.update_inventory) config.register or config.update_all or config.update_inventory
)
# update inventory if feature is enabled # update inventory if feature is enabled
self.inventory = Inventory(server=self) self.inventory = Inventory(server=self)
if update_inventory: if update_inventory:
@ -425,12 +445,16 @@ class ServerBase():
expansion = nb.dcim.devices.get(serial=self.get_expansion_service_tag()) expansion = nb.dcim.devices.get(serial=self.get_expansion_service_tag())
if self.own_expansion_slot() and config.expansion_as_device: if self.own_expansion_slot() and config.expansion_as_device:
logging.debug('Update Server expansion...') logging.debug("Update Server expansion...")
if not expansion: if not expansion:
expansion = self._netbox_create_blade_expansion(chassis, datacenter, tenant, rack) expansion = self._netbox_create_blade_expansion(
chassis, datacenter, tenant, rack
)
# set slot for blade expansion # set slot for blade expansion
self._netbox_set_or_update_blade_expansion_slot(expansion, chassis, datacenter) self._netbox_set_or_update_blade_expansion_slot(
expansion, chassis, datacenter
)
if update_inventory: if update_inventory:
# Updates expansion inventory # Updates expansion inventory
inventory = Inventory(server=self, update_expansion=True) inventory = Inventory(server=self, update_expansion=True)
@ -474,7 +498,7 @@ class ServerBase():
if expansion: if expansion:
update = 0 update = 0
expansion_name = server.name + ' expansion' expansion_name = server.name + " expansion"
if expansion.name != expansion_name: if expansion.name != expansion_name:
expansion.name = expansion_name expansion.name = expansion_name
update += 1 update += 1
@ -482,30 +506,24 @@ class ServerBase():
update += 1 update += 1
if update: if update:
expansion.save() expansion.save()
logging.debug("Finished updating Server!")
myips = nb.ipam.ip_addresses.filter(device_id=server.id)
for ip in myips:
if ip.assigned_object.display == "IPMI":
server.update({'oob_ip': ip.id})
break
logging.debug('Finished updating Server!')
def print_debug(self): def print_debug(self):
self.network = ServerNetwork(server=self) self.network = ServerNetwork(server=self)
print('Datacenter:', self.get_datacenter()) print("Datacenter:", self.get_datacenter())
print('Netbox Datacenter:', self.get_netbox_datacenter()) print("Netbox Datacenter:", self.get_netbox_datacenter())
print('Rack:', self.get_rack()) print("Rack:", self.get_rack())
print('Netbox Rack:', self.get_netbox_rack()) print("Netbox Rack:", self.get_netbox_rack())
print('Is blade:', self.is_blade()) print("Is blade:", self.is_blade())
print('Got expansion:', self.own_expansion_slot()) print("Got expansion:", self.own_expansion_slot())
print('Product Name:', self.get_product_name()) print("Product Name:", self.get_product_name())
print('Platform:', self.device_platform) print("Platform:", self.device_platform)
print('Chassis:', self.get_chassis()) print("Chassis:", self.get_chassis())
print('Chassis service tag:', self.get_chassis_service_tag()) print("Chassis service tag:", self.get_chassis_service_tag())
print('Service tag:', self.get_service_tag()) print("Service tag:", self.get_service_tag())
print('NIC:',) print(
"NIC:",
)
pprint(self.network.get_network_cards()) pprint(self.network.get_network_cards())
pass pass

View file

@ -8,10 +8,10 @@ from netbox_agent.server import ServerBase
class DellHost(ServerBase): class DellHost(ServerBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DellHost, self).__init__(*args, **kwargs) super(DellHost, self).__init__(*args, **kwargs)
self.manufacturer = 'Dell' self.manufacturer = "Dell"
def is_blade(self): def is_blade(self):
return self.get_product_name().startswith('PowerEdge M') return self.get_product_name().startswith("PowerEdge M")
def get_blade_slot(self): def get_blade_slot(self):
""" """
@ -20,48 +20,48 @@ class DellHost(ServerBase):
` Location In Chassis: Slot 03` ` Location In Chassis: Slot 03`
""" """
if self.is_blade(): if self.is_blade():
return self.baseboard[0].get('Location In Chassis').strip() return self.baseboard[0].get("Location In Chassis").strip()
return None return None
def get_chassis_name(self): def get_chassis_name(self):
if not self.is_blade(): if not self.is_blade():
return None return None
return 'Chassis {}'.format(self.get_service_tag()) return "Chassis {}".format(self.get_service_tag())
def get_chassis(self): def get_chassis(self):
if self.is_blade(): if self.is_blade():
return self.chassis[0]['Version'].strip() return self.chassis[0]["Version"].strip()
return self.get_product_name() return self.get_product_name()
def get_chassis_service_tag(self): def get_chassis_service_tag(self):
if self.is_blade(): if self.is_blade():
return self.chassis[0]['Serial Number'].strip() return self.chassis[0]["Serial Number"].strip()
return self.get_service_tag() return self.get_service_tag()
def get_power_consumption(self): def get_power_consumption(self):
''' """
Parse omreport output like this Parse omreport output like this
Amperage Amperage
PS1 Current 1 : 1.8 A PS1 Current 1 : 1.8 A
PS2 Current 2 : 1.4 A PS2 Current 2 : 1.4 A
''' """
value = [] value = []
if not is_tool('omreport'): if not is_tool("omreport"):
logging.error('omreport does not seem to be installed, please debug') logging.error("omreport does not seem to be installed, please debug")
return value return value
data = subprocess.getoutput('omreport chassis pwrmonitoring') data = subprocess.getoutput("omreport chassis pwrmonitoring")
amperage = False amperage = False
for line in data.splitlines(): for line in data.splitlines():
if line.startswith('Amperage'): if line.startswith("Amperage"):
amperage = True amperage = True
continue continue
if amperage: if amperage:
if line.startswith('PS'): if line.startswith("PS"):
amp_value = line.split(':')[1].split()[0] amp_value = line.split(":")[1].split()[0]
value.append(amp_value) value.append(amp_value)
else: else:
break break

View file

@ -5,7 +5,9 @@ from netbox_agent.server import ServerBase
class GenericHost(ServerBase): class GenericHost(ServerBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(GenericHost, self).__init__(*args, **kwargs) super(GenericHost, self).__init__(*args, **kwargs)
self.manufacturer = dmidecode.get_by_type(self.dmi, 'Baseboard')[0].get('Manufacturer') self.manufacturer = dmidecode.get_by_type(self.dmi, "Baseboard")[0].get(
"Manufacturer"
)
def is_blade(self): def is_blade(self):
return False return False

View file

@ -1,6 +1,6 @@
import netbox_agent.dmidecode as dmidecode import netbox_agent.dmidecode as dmidecode
from netbox_agent.server import ServerBase
from netbox_agent.inventory import Inventory from netbox_agent.inventory import Inventory
from netbox_agent.server import ServerBase
class HPHost(ServerBase): class HPHost(ServerBase):
@ -13,8 +13,9 @@ class HPHost(ServerBase):
def is_blade(self): def is_blade(self):
blade = self.product.startswith("ProLiant BL") blade = self.product.startswith("ProLiant BL")
blade |= self.product.startswith("ProLiant m") and \ blade |= self.product.startswith("ProLiant m") and self.product.endswith(
self.product.endswith("Server Cartridge") "Server Cartridge"
)
return blade return blade
def _find_rack_locator(self): def _find_rack_locator(self):
@ -36,7 +37,9 @@ class HPHost(ServerBase):
} }
# HP ProLiant m750, m710x, m510 Server Cartridge # HP ProLiant m750, m710x, m510 Server Cartridge
if self.product.startswith("ProLiant m") and self.product.endswith("Server Cartridge"): if self.product.startswith("ProLiant m") and self.product.endswith(
"Server Cartridge"
):
locator = dmidecode.get_by_type(self.dmi, 2) locator = dmidecode.get_by_type(self.dmi, 2)
chassis = dmidecode.get_by_type(self.dmi, 3) chassis = dmidecode.get_by_type(self.dmi, 3)
return { return {
@ -72,10 +75,14 @@ class HPHost(ServerBase):
""" """
Expansion slot are always the compute bay number + 1 Expansion slot are always the compute bay number + 1
""" """
if self.is_blade() and self.own_gpu_expansion_slot() or \ if (
self.own_disk_expansion_slot() or True: self.is_blade()
return 'Bay {}'.format( and self.own_gpu_expansion_slot()
str(int(self.hp_rack_locator['Server Bay'].strip()) + 1) or self.own_disk_expansion_slot()
or True
):
return "Bay {}".format(
str(int(self.hp_rack_locator["Server Bay"].strip()) + 1)
) )
return None return None
@ -102,7 +109,7 @@ class HPHost(ServerBase):
Indicates if the device hosts a GPU expansion card based Indicates if the device hosts a GPU expansion card based
on the product name on the product name
""" """
return self.get_product_name().endswith('Graphics Exp') return self.get_product_name().endswith("Graphics Exp")
def own_disk_expansion_slot(self): def own_disk_expansion_slot(self):
""" """

View file

@ -4,29 +4,29 @@ from netbox_agent.server import ServerBase
class QCTHost(ServerBase): class QCTHost(ServerBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(QCTHost, self).__init__(*args, **kwargs) super(QCTHost, self).__init__(*args, **kwargs)
self.manufacturer = 'QCT' self.manufacturer = "QCT"
def is_blade(self): def is_blade(self):
return 'Location In Chassis' in self.baseboard[0].keys() return "Location In Chassis" in self.baseboard[0].keys()
def get_blade_slot(self): def get_blade_slot(self):
if self.is_blade(): if self.is_blade():
return 'Slot {}'.format( return "Slot {}".format(
self.baseboard[0].get('Location In Chassis').strip() self.baseboard[0].get("Location In Chassis").strip()
) )
return None return None
def get_chassis_name(self): def get_chassis_name(self):
if not self.is_blade(): if not self.is_blade():
return None return None
return 'Chassis {}'.format(self.get_service_tag()) return "Chassis {}".format(self.get_service_tag())
def get_chassis(self): def get_chassis(self):
if self.is_blade(): if self.is_blade():
return self.chassis[0]['Version'].strip() return self.chassis[0]["Version"].strip()
return self.get_product_name() return self.get_product_name()
def get_chassis_service_tag(self): def get_chassis_service_tag(self):
if self.is_blade(): if self.is_blade():
return self.chassis[0]['Serial Number'].strip() return self.chassis[0]["Serial Number"].strip()
return self.get_service_tag() return self.get_service_tag()

View file

@ -18,22 +18,22 @@ class SupermicroHost(ServerBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SupermicroHost, self).__init__(*args, **kwargs) super(SupermicroHost, self).__init__(*args, **kwargs)
self.manufacturer = 'Supermicro' self.manufacturer = "Supermicro"
def is_blade(self): def is_blade(self):
product_name = self.system[0]['Product Name'].strip() product_name = self.system[0]["Product Name"].strip()
# Blades # Blades
blade = product_name.startswith('SBI') blade = product_name.startswith("SBI")
blade |= product_name.startswith('SBA') blade |= product_name.startswith("SBA")
# Twin # Twin
blade |= 'TR-' in product_name blade |= "TR-" in product_name
# TwinPro # TwinPro
blade |= 'TP-' in product_name blade |= "TP-" in product_name
# BigTwin # BigTwin
blade |= 'BT-' in product_name blade |= "BT-" in product_name
# Microcloud # Microcloud
blade |= product_name.startswith('SYS-5039') blade |= product_name.startswith("SYS-5039")
blade |= product_name.startswith('SYS-5038') blade |= product_name.startswith("SYS-5038")
return blade return blade
def get_blade_slot(self): def get_blade_slot(self):
@ -47,28 +47,28 @@ class SupermicroHost(ServerBase):
def get_service_tag(self): def get_service_tag(self):
if self.is_blade(): if self.is_blade():
return self.baseboard[0]['Serial Number'].strip() return self.baseboard[0]["Serial Number"].strip()
return self.system[0]['Serial Number'].strip() return self.system[0]["Serial Number"].strip()
def get_product_name(self): def get_product_name(self):
if self.is_blade(): if self.is_blade():
return self.baseboard[0]['Product Name'].strip() return self.baseboard[0]["Product Name"].strip()
return self.system[0]['Product Name'].strip() return self.system[0]["Product Name"].strip()
def get_chassis(self): def get_chassis(self):
if self.is_blade(): if self.is_blade():
return self.system[0]['Product Name'].strip() return self.system[0]["Product Name"].strip()
return self.get_product_name() return self.get_product_name()
def get_chassis_service_tag(self): def get_chassis_service_tag(self):
if self.is_blade(): if self.is_blade():
return self.system[0]['Serial Number'].strip() return self.system[0]["Serial Number"].strip()
return self.get_service_tag() return self.get_service_tag()
def get_chassis_name(self): def get_chassis_name(self):
if not self.is_blade(): if not self.is_blade():
return None return None
return 'Chassis {}'.format(self.get_chassis_service_tag()) return "Chassis {}".format(self.get_chassis_service_tag())
def get_expansion_product(self): def get_expansion_product(self):
""" """

View file

@ -1,34 +1,32 @@
import os import os
from pprint import pprint
import netbox_agent.dmidecode as dmidecode import netbox_agent.dmidecode as dmidecode
from netbox_agent.config import config from netbox_agent.config import config
from netbox_agent.config import netbox_instance as nb from netbox_agent.config import netbox_instance as nb
from netbox_agent.location import Tenant from netbox_agent.location import Tenant
from netbox_agent.logging import logging # NOQA from netbox_agent.logging import logging # NOQA
from netbox_agent.misc import create_netbox_tags, get_hostname, get_device_platform from netbox_agent.misc import create_netbox_tags, get_device_platform, get_hostname
from netbox_agent.network import VirtualNetwork from netbox_agent.network import VirtualNetwork
def is_vm(dmi): def is_vm(dmi):
bios = dmidecode.get_by_type(dmi, 'BIOS')[0] bios = dmidecode.get_by_type(dmi, "BIOS")[0]
system = dmidecode.get_by_type(dmi, 'System')[0] system = dmidecode.get_by_type(dmi, "System")[0]
return ( return (
"Hyper-V" in bios["Version"]
or "Xen" in bios["Version"]
or "Google Compute Engine" in system["Product Name"]
) or (
( (
'Hyper-V' in bios['Version'] or "Amazon EC2" in system["Manufacturer"]
'Xen' in bios['Version'] or and not system["Product Name"].endswith(".metal")
'Google Compute Engine' in system['Product Name']
) or
(
(
'Amazon EC2' in system['Manufacturer'] and
not system['Product Name'].endswith('.metal')
) or
'RHEV Hypervisor' in system['Product Name'] or
'QEMU' in system['Manufacturer'] or
'VirtualBox' in bios['Version'] or
'VMware' in system['Manufacturer']
) )
or "RHEV Hypervisor" in system["Product Name"]
or "QEMU" in system["Manufacturer"]
or "VirtualBox" in bios["Version"]
or "VMware" in system["Manufacturer"]
) )
@ -41,13 +39,16 @@ class VirtualMachine(object):
self.network = None self.network = None
self.device_platform = get_device_platform(config.device.platform) self.device_platform = get_device_platform(config.device.platform)
self.tags = list(set(config.device.tags.split(','))) if config.device.tags else [] self.tags = (
if self.tags and len(self.tags): list(set(config.device.tags.split(","))) if config.device.tags else []
create_netbox_tags(self.tags) )
self.nb_tags = create_netbox_tags(self.tags)
def get_memory(self): def get_memory(self):
mem_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') # e.g. 4015976448 mem_bytes = os.sysconf("SC_PAGE_SIZE") * os.sysconf(
mem_gib = mem_bytes / (1024.**2) # e.g. 3.74 "SC_PHYS_PAGES"
) # e.g. 4015976448
mem_gib = mem_bytes / (1024.0**2) # e.g. 3.74
return int(mem_gib) return int(mem_gib)
def get_vcpus(self): def get_vcpus(self):
@ -55,9 +56,7 @@ class VirtualMachine(object):
def get_netbox_vm(self): def get_netbox_vm(self):
hostname = get_hostname(config) hostname = get_hostname(config)
vm = nb.virtualization.virtual_machines.get( vm = nb.virtualization.virtual_machines.get(name=hostname)
name=hostname
)
return vm return vm
def get_netbox_cluster(self, name): def get_netbox_cluster(self, name):
@ -80,13 +79,11 @@ class VirtualMachine(object):
tenant = self.get_tenant() tenant = self.get_tenant()
if tenant is None: if tenant is None:
return None return None
nb_tenant = nb.tenancy.tenants.get( nb_tenant = nb.tenancy.tenants.get(slug=self.get_tenant())
slug=self.get_tenant()
)
return nb_tenant return nb_tenant
def netbox_create_or_update(self, config): def netbox_create_or_update(self, config):
logging.debug('It\'s a virtual machine') logging.debug("It's a virtual machine")
created = False created = False
updated = 0 updated = 0
@ -97,7 +94,7 @@ class VirtualMachine(object):
memory = self.get_memory() memory = self.get_memory()
tenant = self.get_netbox_tenant() tenant = self.get_netbox_tenant()
if not vm: if not vm:
logging.debug('Creating Virtual machine..') logging.debug("Creating Virtual machine..")
cluster = self.get_netbox_cluster(config.virtual.cluster_name) cluster = self.get_netbox_cluster(config.virtual.cluster_name)
vm = nb.virtualization.virtual_machines.create( vm = nb.virtualization.virtual_machines.create(
@ -107,7 +104,7 @@ class VirtualMachine(object):
vcpus=vcpus, vcpus=vcpus,
memory=memory, memory=memory,
tenant=tenant.id if tenant else None, tenant=tenant.id if tenant else None,
tags=self.tags, tags=[{"name": x} for x in self.tags],
) )
created = True created = True
@ -121,12 +118,34 @@ class VirtualMachine(object):
if vm.memory != memory: if vm.memory != memory:
vm.memory = memory vm.memory = memory
updated += 1 updated += 1
if sorted(set(vm.tags)) != sorted(set(self.tags)):
vm.tags = self.tags vm_tags = sorted(set([x.name for x in vm.tags]))
tags = sorted(set(self.tags))
if vm_tags != tags:
new_tags_ids = [x.id for x in self.nb_tags]
if not config.preserve_tags:
vm.tags = new_tags_ids
else:
vm_tags_ids = [x.id for x in vm.tags]
vm.tags = sorted(set(new_tags_ids + vm_tags_ids))
updated += 1 updated += 1
if vm.platform != self.device_platform: if vm.platform != self.device_platform:
vm.platform = self.device_platform vm.platform = self.device_platform
updated += 1 updated += 1
if updated: if updated:
vm.save() vm.save()
def print_debug(self):
self.network = VirtualNetwork(server=self)
print("Cluster:", self.get_netbox_cluster(config.virtual.cluster_name))
print("Platform:", self.device_platform)
print("VM:", self.get_netbox_vm())
print("vCPU:", self.get_vcpus())
print("Memory:", f"{self.get_memory()} MB")
print(
"NIC:",
)
pprint(self.network.get_network_cards())
pass

46
nix/netifaces2.nix Normal file
View file

@ -0,0 +1,46 @@
{
lib,
buildPythonPackage,
fetchFromGitHub,
cargo,
rustPlatform,
rustc,
typing-extensions,
}:
buildPythonPackage rec {
pname = "netifaces-2";
version = "0.0.22";
pyproject = true;
src = fetchFromGitHub {
owner = "SamuelYvon";
repo = "netifaces-2";
rev = "V${version}";
hash = "sha256-XO3HWq8FOVzvpbK8mIBOup6hFMnhDpqOK/5bPziPZQ8=";
};
cargoDeps = rustPlatform.fetchCargoTarball {
inherit src;
name = "${pname}-${version}";
hash = "sha256-uoUa6DSBuIV3RrE7svT1TVLxPHdx8BFu/C6mbpRmor0=";
};
build-system = [
cargo
rustPlatform.cargoSetupHook
rustPlatform.maturinBuildHook
rustc
];
dependencies = [ typing-extensions ];
pythonImportsCheck = [ "netifaces" ];
meta = {
description = "Netifaces reborn";
homepage = "https://github.com/SamuelYvon/netifaces-2.git";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ ];
};
}

80
npins/default.nix Normal file
View file

@ -0,0 +1,80 @@
# Generated by npins. Do not modify; will be overwritten regularly
let
data = builtins.fromJSON (builtins.readFile ./sources.json);
version = data.version;
mkSource =
spec:
assert spec ? type;
let
path =
if spec.type == "Git" then
mkGitSource spec
else if spec.type == "GitRelease" then
mkGitSource spec
else if spec.type == "PyPi" then
mkPyPiSource spec
else if spec.type == "Channel" then
mkChannelSource spec
else
builtins.throw "Unknown source type ${spec.type}";
in
spec // { outPath = path; };
mkGitSource =
{
repository,
revision,
url ? null,
hash,
branch ? null,
...
}:
assert repository ? type;
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
# In the latter case, there we will always be an url to the tarball
if url != null then
(builtins.fetchTarball {
inherit url;
sha256 = hash; # FIXME: check nix version & use SRI hashes
})
else
assert repository.type == "Git";
let
urlToName =
url: rev:
let
matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url;
short = builtins.substring 0 7 rev;
appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else "";
in
"${if matched == null then "source" else builtins.head matched}${appendShort}";
name = urlToName repository.url revision;
in
builtins.fetchGit {
url = repository.url;
rev = revision;
inherit name;
# hash = hash;
};
mkPyPiSource =
{ url, hash, ... }:
builtins.fetchurl {
inherit url;
sha256 = hash;
};
mkChannelSource =
{ url, hash, ... }:
builtins.fetchTarball {
inherit url;
sha256 = hash;
};
in
if version == 3 then
builtins.mapAttrs (_: mkSource) data.pins
else
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"

23
npins/sources.json Normal file
View file

@ -0,0 +1,23 @@
{
"pins": {
"git-hooks": {
"type": "Git",
"repository": {
"type": "GitHub",
"owner": "cachix",
"repo": "git-hooks.nix"
},
"branch": "master",
"revision": "3c3e88f0f544d6bb54329832616af7eb971b6be6",
"url": "https://github.com/cachix/git-hooks.nix/archive/3c3e88f0f544d6bb54329832616af7eb971b6be6.tar.gz",
"hash": "04pwjz423iq2nkazkys905gvsm5j39722ngavrnx42b8msr5k555"
},
"nixpkgs": {
"type": "Channel",
"name": "nixpkgs-unstable",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre694416.ccc0c2126893/nixexprs.tar.xz",
"hash": "0cn1z4wzps8nfqxzr6l5mbn81adcqy2cy2ic70z13fhzicmxfsbx"
}
},
"version": 3
}

2
pyproject.toml Normal file
View file

@ -0,0 +1,2 @@
[tool.isort]
profile = "black"

View file

@ -1,42 +1,38 @@
from setuptools import find_packages, setup
import os import os
from setuptools import find_packages, setup
def get_requirements(): def get_requirements():
reqs_path = os.path.join( reqs_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
os.path.dirname(__file__), with open(reqs_path, "r") as f:
'requirements.txt' reqs = [r.strip() for r in f if r.strip()]
)
with open(reqs_path, 'r') as f:
reqs = [
r.strip() for r in f
if r.strip()
]
return reqs return reqs
setup( setup(
name='netbox_agent', name="netbox_agent",
version='0.7.1', version="0.7.1",
description='NetBox agent for server', description="NetBox agent for server",
long_description=open('README.md', encoding="utf-8").read(), long_description=open("README.md", encoding="utf-8").read(),
long_description_content_type='text/markdown', long_description_content_type="text/markdown",
url='https://github.com/solvik/netbox_agent', url="https://github.com/solvik/netbox_agent",
author='Solvik Blum', author="Solvik Blum",
author_email='solvik@solvik.fr', author_email="solvik@solvik.fr",
license='Apache2', license="Apache2",
include_package_data=True, include_package_data=True,
packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
use_scm_version=True, use_scm_version=True,
install_requires=get_requirements(), install_requires=get_requirements(),
zip_safe=False, zip_safe=False,
keywords=['netbox'], keywords=["netbox"],
classifiers=[ classifiers=[
'Intended Audience :: Developers', "Intended Audience :: Developers",
'Development Status :: 5 - Production/Stable', "Development Status :: 5 - Production/Stable",
'Programming Language :: Python :: 3', "Programming Language :: Python :: 3",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: 3.6",
], ],
entry_points={ entry_points={
'console_scripts': ['netbox_agent=netbox_agent.cli:main'], "console_scripts": ["netbox_agent=netbox_agent.cli:main"],
} },
) )

1
shell.nix Normal file
View file

@ -0,0 +1 @@
(import ./. { }).devShell

View file

@ -14,14 +14,15 @@ def get_fixture_paths(path):
return fixture_paths return fixture_paths
def parametrize_with_fixtures(path, base_path='tests/fixtures', def parametrize_with_fixtures(
argname='fixture', only_filenames=None): path, base_path="tests/fixtures", argname="fixture", only_filenames=None
):
path = os.path.join(base_path, path) path = os.path.join(base_path, path)
fixture_paths = get_fixture_paths(path) fixture_paths = get_fixture_paths(path)
argvalues = [] argvalues = []
for path in fixture_paths: for path in fixture_paths:
with open(path, 'r') as f: with open(path, "r") as f:
content = ''.join(f.readlines()) content = "".join(f.readlines())
filename = os.path.basename(path) filename = os.path.basename(path)
if only_filenames and filename not in only_filenames: if only_filenames and filename not in only_filenames:
continue continue
@ -30,4 +31,5 @@ def parametrize_with_fixtures(path, base_path='tests/fixtures',
def _decorator(test_function): def _decorator(test_function):
return pytest.mark.parametrize(argname, argvalues)(test_function) return pytest.mark.parametrize(argname, argvalues)(test_function)
return _decorator return _decorator

View file

@ -3,18 +3,22 @@ from tests.conftest import parametrize_with_fixtures
@parametrize_with_fixtures( @parametrize_with_fixtures(
'lldp/', only_filenames=[ "lldp/",
'dedibox1.txt', only_filenames=[
]) "dedibox1.txt",
],
)
def test_lldp_parse_with_port_desc(fixture): def test_lldp_parse_with_port_desc(fixture):
lldp = LLDP(fixture) lldp = LLDP(fixture)
assert lldp.get_switch_port('enp1s0f0') == 'RJ-9' assert lldp.get_switch_port("enp1s0f0") == "RJ-9"
@parametrize_with_fixtures( @parametrize_with_fixtures(
'lldp/', only_filenames=[ "lldp/",
'qfx.txt', only_filenames=[
]) "qfx.txt",
],
)
def test_lldp_parse_without_ifname(fixture): def test_lldp_parse_without_ifname(fixture):
lldp = LLDP(fixture) lldp = LLDP(fixture)
assert lldp.get_switch_port('eth0') == 'xe-0/0/1' assert lldp.get_switch_port("eth0") == "xe-0/0/1"

View file

@ -6,7 +6,7 @@ from netbox_agent.vendors.supermicro import SupermicroHost
from tests.conftest import parametrize_with_fixtures from tests.conftest import parametrize_with_fixtures
@parametrize_with_fixtures('dmidecode/') @parametrize_with_fixtures("dmidecode/")
def test_init(fixture): def test_init(fixture):
dmi = parse(fixture) dmi = parse(fixture)
server = ServerBase(dmi) server = ServerBase(dmi)
@ -14,96 +14,78 @@ def test_init(fixture):
@parametrize_with_fixtures( @parametrize_with_fixtures(
'dmidecode/', only_filenames=[ "dmidecode/",
'HP_SL4540_Gen8', only_filenames=[
'HP_BL460c_Gen9', "HP_SL4540_Gen8",
'HP_DL380p_Gen8', "HP_BL460c_Gen9",
'HP_SL4540_Gen8' "HP_DL380p_Gen8",
'HP_ProLiant_BL460c_Gen10_Graphics_Exp' "HP_SL4540_Gen8" "HP_ProLiant_BL460c_Gen10_Graphics_Exp",
]) ],
)
def test_hp_service_tag(fixture): def test_hp_service_tag(fixture):
dmi = parse(fixture) dmi = parse(fixture)
server = HPHost(dmi) server = HPHost(dmi)
assert server.get_service_tag() == '4242' assert server.get_service_tag() == "4242"
@parametrize_with_fixtures( @parametrize_with_fixtures("dmidecode/", only_filenames=["HP_ProLiant_m710x"])
'dmidecode/', only_filenames=[
'HP_ProLiant_m710x'
])
def test_moonshot_blade(fixture): def test_moonshot_blade(fixture):
dmi = parse(fixture) dmi = parse(fixture)
server = HPHost(dmi) server = HPHost(dmi)
assert server.get_service_tag() == 'CN66480BLA' assert server.get_service_tag() == "CN66480BLA"
assert server.get_chassis_service_tag() == 'CZ3702MD5K' assert server.get_chassis_service_tag() == "CZ3702MD5K"
assert server.is_blade() is True assert server.is_blade() is True
assert server.own_expansion_slot() is False assert server.own_expansion_slot() is False
@parametrize_with_fixtures( @parametrize_with_fixtures("dmidecode/", only_filenames=["SYS-5039MS-H12TRF-OS012.txt"])
'dmidecode/', only_filenames=[
'SYS-5039MS-H12TRF-OS012.txt'
])
def test_supermicro_blade(fixture): def test_supermicro_blade(fixture):
dmi = parse(fixture) dmi = parse(fixture)
server = SupermicroHost(dmi) server = SupermicroHost(dmi)
assert server.get_service_tag() == 'E235735X6B01665' assert server.get_service_tag() == "E235735X6B01665"
assert server.get_chassis_service_tag() == 'C9390AF40A20098' assert server.get_chassis_service_tag() == "C9390AF40A20098"
assert server.get_chassis() == 'SYS-5039MS-H12TRF-OS012' assert server.get_chassis() == "SYS-5039MS-H12TRF-OS012"
assert server.is_blade() is True assert server.is_blade() is True
@parametrize_with_fixtures( @parametrize_with_fixtures("dmidecode/", only_filenames=["SM_SYS-6018R"])
'dmidecode/', only_filenames=[
'SM_SYS-6018R'
])
def test_supermicro_pizza(fixture): def test_supermicro_pizza(fixture):
dmi = parse(fixture) dmi = parse(fixture)
server = SupermicroHost(dmi) server = SupermicroHost(dmi)
assert server.get_service_tag() == 'A177950X7709591' assert server.get_service_tag() == "A177950X7709591"
assert server.get_chassis() == 'SYS-6018R-TDTPR' assert server.get_chassis() == "SYS-6018R-TDTPR"
assert server.is_blade() is False assert server.is_blade() is False
@parametrize_with_fixtures( @parametrize_with_fixtures("dmidecode/", only_filenames=["QCT_X10E-9N"])
'dmidecode/', only_filenames=[
'QCT_X10E-9N'
])
def test_qct_x10(fixture): def test_qct_x10(fixture):
dmi = parse(fixture) dmi = parse(fixture)
server = QCTHost(dmi) server = QCTHost(dmi)
assert server.get_service_tag() == 'QTFCQ57140285' assert server.get_service_tag() == "QTFCQ57140285"
@parametrize_with_fixtures( @parametrize_with_fixtures("dmidecode/", only_filenames=["unknown.txt"])
'dmidecode/', only_filenames=[
'unknown.txt'
])
def test_generic_host_service_tag(fixture): def test_generic_host_service_tag(fixture):
dmi = parse(fixture) dmi = parse(fixture)
server = ServerBase(dmi) server = ServerBase(dmi)
assert server.get_service_tag() == '42' assert server.get_service_tag() == "42"
@parametrize_with_fixtures( @parametrize_with_fixtures("dmidecode/", only_filenames=["unknown.txt"])
'dmidecode/', only_filenames=[
'unknown.txt'
])
def test_generic_host_product_name(fixture): def test_generic_host_product_name(fixture):
dmi = parse(fixture) dmi = parse(fixture)
server = ServerBase(dmi) server = ServerBase(dmi)
assert server.get_product_name() == 'SR' assert server.get_product_name() == "SR"
@parametrize_with_fixtures( @parametrize_with_fixtures(
'dmidecode/', only_filenames=[ "dmidecode/", only_filenames=["HP_ProLiant_BL460c_Gen10_Graphics_Exp"]
'HP_ProLiant_BL460c_Gen10_Graphics_Exp' )
])
def test_hp_blade_with_gpu_expansion(fixture): def test_hp_blade_with_gpu_expansion(fixture):
dmi = parse(fixture) dmi = parse(fixture)
server = HPHost(dmi) server = HPHost(dmi)
assert server.get_service_tag() == '4242' assert server.get_service_tag() == "4242"
assert server.get_chassis_service_tag() == '4343' assert server.get_chassis_service_tag() == "4343"
assert server.is_blade() is True assert server.is_blade() is True
assert server.own_expansion_slot() is True assert server.own_expansion_slot() is True
assert server.get_expansion_service_tag() == '4242 expansion' assert server.get_expansion_service_tag() == "4242 expansion"