Merge branch 'master' into feature/network

This commit is contained in:
Solvik 2019-08-04 20:27:58 +02:00 committed by GitHub
commit 4ddbb89c18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 135 additions and 23 deletions

View file

@ -2,7 +2,29 @@
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 +62,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')

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)

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

@ -2,6 +2,7 @@ from pprint import pprint
import socket
from netbox_agent.config import netbox_instance as nb
from netbox_agent.datacenter import Datacenter
import netbox_agent.dmidecode as dmidecode
from netbox_agent.network import Network
@ -17,6 +18,16 @@ class ServerBase():
self.network = Network(server=self)
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
@ -59,39 +70,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',
)
@ -100,15 +105,12 @@ 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
@ -116,6 +118,7 @@ class ServerBase():
return nb.dcim.devices.get(serial=self.get_service_tag())
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())
@ -125,9 +128,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(
@ -139,7 +142,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()