add datacenter location awareness with pseudo-driver (cmd and file) #4

Merged
Solvik merged 10 commits from feature/datacenter into master 2019-08-04 20:09:29 +02:00
8 changed files with 135 additions and 24 deletions

View file

@ -2,7 +2,28 @@
This project aims to create hardware automatically into Netbox based on standard tools (dmidecode, lldpd, parsing /sys/, etc).
The goal is to generate an existing infrastructure on Netbox and have the ability to update it regularly.
The goal is to generate an existing infrastructure on Netbox and have the ability to update it regularly by executing the agent.
# Features
* Create servers, chassis and blade through standard tools (`dmidecode`)
* Create physical network interfaces with IPs
* Generic ability to guess datacenters through drivers (`cmd` and `file` and custom ones)
# Configuration
```netbox:
url: 'http://netbox.internal.company.com'
token: supersecrettoken
datacenter_location:
# driver_file: /opt/netbox_driver_dc.py
driver: file:/etc/qualification
regex: "datacenter: (?P<datacenter>[A-Za-z0-9]+)"
# driver: 'cmd:lldpctl'
# regex = 'SysName: .*\.(?P<datacenter>[A-Za-z0-9]+)'```
```
# Hardware
@ -40,14 +61,9 @@ Tested on:
# TODO
- [ ] HP(E) servers support
- [ ] Handle blade moving
- [ ] Handle network cards (MAC, IP addresses)
- [ ] Handle switch <> NIC connections (using lldp)
- [ ] Handle blade and server local changes (new NIC, new RAM, etc) using somekind of diff
# Ideas
- [ ] CPU, RAID Card(s), RAM, Disks in `Device`'s `Inventory`
- [ ] `CustomFields` support with firmware versions for Device (BIOS), RAID Cards and disks
- [ ] Handle custom business logic : datacenter guessing logic based on hostname/switch name

12
netbox_agent.yaml.example Normal file
View file

@ -0,0 +1,12 @@
netbox:
url: 'http://netbox.internal.company.com'
token: supersecrettoken
datacenter_location:
driver: "cmd:cat /etc/qualification | tr [a-z] [A-Z]"
regex: "DATACENTER: (?P<datacenter>[A-Za-z0-9]+)"
# driver: 'cmd:lldpctl'
# regex: 'SysName: .*\.([A-Za-z0-9]+)'
#
# driver: "file:/tmp/datacenter"
# regex: "(.*)"

16
netbox_agent/config.py Normal file
View file

@ -0,0 +1,16 @@
import pynetbox
import yaml
with open('/etc/netbox_agent.yaml', 'r') as ymlfile:
# FIXME: validate configuration file
config = yaml.load(ymlfile)
netbox_instance = pynetbox.api(
url=config['netbox']['url'],
token=config['netbox']['token']
)
DATACENTER_LOCATION_DRIVER_FILE = config.get('datacenter_location').get('driver_file')
DATACENTER_LOCATION = config.get('datacenter_location').get('driver')
DATACENTER_LOCATION_REGEX = config.get('datacenter_location').get('regex')
blotus commented 2019-08-04 18:58:28 +02:00 (Migrated from github.com)
Review

Probably need some sanity check as the drivers (for now) require the regex to have a datacenter named group.
I think there is 3 solutions :

  • Check if the regex has a datacenter named group (not a big fan)
  • Remove the requirement of the named capture group, and just grab whatever is in the first capturing group of the regex.
  • Allow user to configure the name of group or the position in the configuration file (and defaulting to the 1st capturing group)
Probably need some sanity check as the drivers (for now) require the regex to have a `datacenter` named group. I think there is 3 solutions : - Check if the regex has a `datacenter` named group (not a big fan) - Remove the requirement of the named capture group, and just grab whatever is in the first capturing group of the regex. - Allow user to configure the name of group or the position in the configuration file (and defaulting to the 1st capturing group)
Solvik commented 2019-08-04 19:52:31 +02:00 (Migrated from github.com)
Review

fixed in 9f0db28
for the sanity check, will address that in a PR around configuration file

fixed in 9f0db28 for the sanity check, will address that in a PR around configuration file

View file

@ -0,0 +1,46 @@
import importlib
import importlib.machinery
from netbox_agent.config import DATACENTER_LOCATION, DATACENTER_LOCATION_DRIVER_FILE, \
DATACENTER_LOCATION_REGEX
class Datacenter():
"""
This class is used to guess the datacenter in order to push the information
in Netbox for a `Device`
A driver takes a `value` and evaluates a regex with a `capture group`.
There's embeded drivers such as `file` or `cmd` which read a file or return the
output of a file.
There's also a support for an external driver file outside of this project in case
the logic isn't supported here.
"""
def __init__(self, *args, **kwargs):
self.driver = DATACENTER_LOCATION.split(':')[0]
self.driver_value = ':'.join(DATACENTER_LOCATION.split(':')[1:])
self.driver_file = DATACENTER_LOCATION_DRIVER_FILE
if self.driver_file:
try:
# FIXME: Works with Python 3.3+, support older version?
loader = importlib.machinery.SourceFileLoader('driver_file', self.driver_file)
self.driver = loader.load_module()
except ImportError:
raise ImportError("Couldn't import {} as a module".format(self.driver_file))
else:
try:
self.driver = importlib.import_module(
'netbox_agent.drivers.datacenter_{}'.format(self.driver)
)
except ImportError:
raise ImportError("Driver {} doesn't exists".format(self.driver))
def get(self):
if not hasattr(self.driver, 'get'):
raise Exception(
"Your driver {} doesn't have a get() function, please fix it".format(self.driver)
)
return getattr(self.driver, 'get')(self.driver_value, DATACENTER_LOCATION_REGEX)
blotus commented 2019-08-04 19:00:16 +02:00 (Migrated from github.com)
Review

We should properly check if the module has a get method to output a proper error message if a custom driver is not compliant instead of an ugly stacktrace.

We should properly check if the module has a `get` method to output a proper error message if a custom driver is not compliant instead of an ugly stacktrace.
Solvik commented 2019-08-04 19:19:14 +02:00 (Migrated from github.com)
Review

Fixed in last commit

Fixed in last commit

View file

View file

@ -0,0 +1,10 @@
import re
import subprocess
def get(value, regex):
output = subprocess.getoutput(value)
r = re.search(regex, output)
if r and len(r.groups()) > 0:
return r.groups()[0]
return None

View file

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

View file

@ -3,6 +3,7 @@ import re
import socket
from netbox_agent.config import netbox_instance as nb
from netbox_agent.datacenter import Datacenter
import netbox_agent.dmidecode as dmidecode
# Regex to match base interface name
@ -21,6 +22,16 @@ class ServerBase():
self.network_cards = []
def get_datacenter(self):
dc = Datacenter()
return dc.get()
def get_netbox_datacenter(self):
datacenter = nb.dcim.sites.get(
slug=self.get_datacenter()
)
return datacenter
def get_product_name(self):
"""
Return the Chassis Name from dmidecode info
@ -66,7 +77,7 @@ class ServerBase():
nics.append(nic)
return nics
def _netbox_create_blade_chassis(self):
def _netbox_create_blade_chassis(self, datacenter):
device_type = nb.dcim.device_types.get(
model=self.get_chassis(),
)
@ -75,39 +86,33 @@ class ServerBase():
device_role = nb.dcim.device_roles.get(
name='Server Chassis',
)
datacenter = nb.dcim.sites.get(
name='DC3', # FIXME: datacenter support
)
new_chassis = nb.dcim.devices.create(
name=''.format(),
device_type=device_type.id,
serial=self.get_chassis_service_tag(),
device_role=device_role.id,
site=datacenter.id,
site=datacenter.id if datacenter else None,
)
return new_chassis
def _netbox_create_blade(self, chassis):
def _netbox_create_blade(self, chassis, datacenter):
device_role = nb.dcim.device_roles.get(
name='Blade',
)
device_type = nb.dcim.device_types.get(
model=self.get_product_name(),
)
datacenter = nb.dcim.sites.get(
name='DC3', # FIXME: datacenter support
)
new_blade = nb.dcim.devices.create(
name='{}'.format(socket.gethostname()),
serial=self.get_service_tag(),
device_role=device_role.id,
device_type=device_type.id,
parent_device=chassis.id,
site=datacenter.id,
site=datacenter.id if datacenter else None,
)
return new_blade
def _netbox_create_server(self):
def _netbox_create_server(self, datacenter):
device_role = nb.dcim.device_roles.get(
name='Server',
)
@ -116,19 +121,17 @@ class ServerBase():
)
if not device_type:
raise Exception('Chassis "{}" doesn\'t exist'.format(self.get_chassis()))
datacenter = nb.dcim.sites.get(
name='DC3' # FIXME: datacenter support
)
new_server = nb.dcim.devices.create(
name='{}'.format(socket.gethostname()),
serial=self.get_service_tag(),
device_role=device_role.id,
device_type=device_type.id,
site=datacenter.id,
site=datacenter.id if datacenter else None,
)
return new_server
def netbox_create(self):
datacenter = self.get_netbox_datacenter()
if self.is_blade():
# let's find the blade
blade = nb.dcim.devices.get(serial=self.get_service_tag())
@ -138,9 +141,9 @@ class ServerBase():
# check if the chassis exist before
# if it doesn't exist, create it
if not chassis:
chassis = self._netbox_create_blade_chassis()
chassis = self._netbox_create_blade_chassis(datacenter)
blade = self._netbox_create_blade(chassis)
blade = self._netbox_create_blade(chassis, datacenter)
# Find the slot and update it with our blade
device_bays = nb.dcim.device_bays.filter(
@ -152,7 +155,6 @@ class ServerBase():
device_bay.installed_device = blade
device_bay.save()
else:
# FIXME : handle pizza box
server = nb.dcim.devices.get(serial=self.get_service_tag())
if not server:
self._netbox_create_server()