Merge pull request #4 from Solvik/feature/datacenter
add datacenter location awareness with pseudo-driver (cmd and file)
This commit is contained in:
commit
974c2b0c75
8 changed files with 135 additions and 24 deletions
28
README.md
28
README.md
|
@ -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
12
netbox_agent.yaml.example
Normal 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
16
netbox_agent/config.py
Normal 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')
|
46
netbox_agent/datacenter.py
Normal file
46
netbox_agent/datacenter.py
Normal 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)
|
0
netbox_agent/drivers/__init__.py
Normal file
0
netbox_agent/drivers/__init__.py
Normal file
10
netbox_agent/drivers/datacenter_cmd.py
Normal file
10
netbox_agent/drivers/datacenter_cmd.py
Normal 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
|
9
netbox_agent/drivers/datacenter_file.py
Normal file
9
netbox_agent/drivers/datacenter_file.py
Normal 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
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue