Initial push to public repo

This commit is contained in:
Jeremy Stretch
2016-03-01 11:23:03 -05:00
commit 27b289ee3b
281 changed files with 26061 additions and 0 deletions

View File

13
netbox/extras/admin.py Normal file
View File

@@ -0,0 +1,13 @@
from django.contrib import admin
from .models import Graph, ExportTemplate
@admin.register(Graph)
class GraphAdmin(admin.ModelAdmin):
list_display = ['name', 'type', 'weight', 'source']
@admin.register(ExportTemplate)
class ExportTemplateAdmin(admin.ModelAdmin):
list_display = ['content_type', 'name', 'mime_type', 'file_extension']

View File

View File

@@ -0,0 +1,31 @@
from rest_framework import renderers
# IP address family designations
AF = {
4: 'A',
6: 'AAAA',
}
class BINDZoneRenderer(renderers.BaseRenderer):
"""
Generate a BIND zone file from a list of DNS records.
Required fields: `name`, `primary_ip`
"""
media_type = 'text/plain'
format = 'bind-zone'
def render(self, data, media_type=None, renderer_context=None):
records = []
for record in data:
if record.get('name') and record.get('primary_ip'):
try:
records.append("{} IN {} {}".format(
record['name'],
AF[record['primary_ip']['family']],
record['primary_ip']['address'].split('/')[0],
))
except KeyError:
pass
return '\n'.join(records)

View File

@@ -0,0 +1,14 @@
from rest_framework import serializers
from extras.models import Graph
class GraphSerializer(serializers.ModelSerializer):
embed_url = serializers.SerializerMethodField()
class Meta:
model = Graph
fields = ['name', 'embed_url', 'link']
def get_embed_url(self, obj):
return obj.embed_url(self.context['graphed_object'])

View File

@@ -0,0 +1,33 @@
from rest_framework import generics
from django.http import Http404
from django.shortcuts import get_object_or_404
from circuits.models import Provider
from dcim.models import Site, Interface
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_PROVIDER, GRAPH_TYPE_SITE
from .serializers import GraphSerializer
class GraphListView(generics.ListAPIView):
"""
Returns a list of relevant graphs
"""
serializer_class = GraphSerializer
def get_serializer_context(self):
cls = {
GRAPH_TYPE_INTERFACE: Interface,
GRAPH_TYPE_PROVIDER: Provider,
GRAPH_TYPE_SITE: Site,
}
context = super(GraphListView, self).get_serializer_context()
context.update({'graphed_object': get_object_or_404(cls[self.kwargs.get('type')], pk=self.kwargs['pk'])})
return context
def get_queryset(self):
graph_type = self.kwargs.get('type', None)
if not graph_type:
raise Http404()
queryset = Graph.objects.filter(type=graph_type)
return queryset

View File

@@ -0,0 +1,12 @@
- model: extras.graph
pk: 1
fields: {type: 300, weight: 1000, name: Site Test Graph, source: 'http://localhost/na.png',
link: ''}
- model: extras.graph
pk: 2
fields: {type: 200, weight: 1000, name: Provider Test Graph, source: 'http://localhost/provider_graph.png',
link: ''}
- model: extras.graph
pk: 3
fields: {type: 100, weight: 1000, name: Interface Test Graph, source: 'http://localhost/interface_graph.png',
link: ''}

View File

View File

@@ -0,0 +1,117 @@
from Exscript.protocols.Exception import LoginFailure
from getpass import getpass
from ncclient.transport.errors import AuthenticationError
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from dcim.models import Device, Module, Site
class Command(BaseCommand):
help = "Update inventory information for specified devices"
username = settings.NETBOX_USERNAME
password = settings.NETBOX_PASSWORD
def add_arguments(self, parser):
parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")
parser.add_argument('-p', '--password', action='store_true', default=False, help="Prompt for password to use")
parser.add_argument('-s', '--site', dest='site', action='append', help="Filter devices by site (include argument once per site)")
parser.add_argument('-n', '--name', dest='name', help="Filter devices by name (regular expression)")
parser.add_argument('--full', action='store_true', default=False, help="For inventory update for all devices")
parser.add_argument('--fake', action='store_true', default=False, help="Do not actually update database")
def handle(self, *args, **options):
# Credentials
if options['username']:
self.username = options['username']
if options['password']:
self.password = getpass("Password: ")
device_list = Device.objects.filter()
# --site: Include only devices belonging to specified site(s)
if options['site']:
sites = Site.objects.filter(slug__in=options['site'])
if sites:
site_names = [s.name for s in sites]
self.stdout.write("Running inventory for these sites: {}".format(', '.join(site_names)))
else:
raise CommandError("One or more sites specified but none found.")
device_list = device_list.filter(rack__site__in=sites)
# --name: Filter devices by name matching a regex
if options['name']:
device_list = device_list.filter(name__iregex=options['name'])
# --full: Gather inventory data for *all* devices
if options['full']:
self.stdout.write("WARNING: Running inventory for all devices! Prior data will be overwritten. (--full)")
# --fake: Gathering data but not updating the database
if options['fake']:
self.stdout.write("WARNING: Inventory data will not be saved! (--fake)")
device_count = device_list.count()
self.stdout.write("** Found {} devices...".format(device_count))
for i, device in enumerate(device_list, start=1):
self.stdout.write("[{}/{}] {}: ".format(i, device_count, device.name), ending='')
# Skip inactive devices
if not device.status:
self.stdout.write("Skipped (inactive)")
continue
# Skip devices without primary_ip set
if not device.primary_ip:
self.stdout.write("Skipped (no primary IP set)")
continue
# Skip devices which have already been inventoried if not doing a full update
if device.serial and not options['full']:
self.stdout.write("Skipped (Serial: {})".format(device.serial))
continue
RPC = device.get_rpc_client()
if not RPC:
self.stdout.write("Skipped (no RPC client available for platform {})".format(device.platform))
continue
# Connect to device and retrieve inventory info
try:
with RPC(device, self.username, self.password) as rpc_client:
inventory = rpc_client.get_inventory()
except KeyboardInterrupt:
raise
except (AuthenticationError, LoginFailure):
self.stdout.write("Authentication error!")
continue
except Exception as e:
self.stdout.write("Error for {} ({}): {}".format(device, device.primary_ip.address.ip, e))
continue
self.stdout.write("")
self.stdout.write("\tSerial: {}".format(inventory['chassis']['serial']))
self.stdout.write("\tDescription: {}".format(inventory['chassis']['description']))
for module in inventory['modules']:
self.stdout.write("\tModule: {} / {} ({})".format(module['name'], module['part_id'], module['serial']))
if not options['fake']:
with transaction.atomic():
if inventory['chassis']['serial']:
device.serial = inventory['chassis']['serial']
device.save()
Module.objects.filter(device=device).delete()
modules = []
for module in inventory['modules']:
modules.append(Module(device=device,
name=module['name'],
part_id=module['part_id'],
serial=module['serial']))
Module.objects.bulk_create(modules)
self.stdout.write("Finished!")

View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-02-27 02:35
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='ExportTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('template_code', models.TextField()),
('mime_type', models.CharField(blank=True, max_length=15)),
('file_extension', models.CharField(blank=True, max_length=15)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
options={
'ordering': ['content_type', 'name'],
},
),
migrations.CreateModel(
name='Graph',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.PositiveSmallIntegerField(choices=[(100, b'Interface'), (200, b'Provider'), (300, b'Site')])),
('weight', models.PositiveSmallIntegerField(default=1000)),
('name', models.CharField(max_length=100, verbose_name=b'Name')),
('source', models.CharField(max_length=500, verbose_name=b'Source URL')),
('link', models.URLField(blank=True, verbose_name=b'Link URL')),
],
options={
'ordering': ['type', 'weight', 'name'],
},
),
migrations.AlterUniqueTogether(
name='exporttemplate',
unique_together=set([('content_type', 'name')]),
),
]

View File

70
netbox/extras/models.py Normal file
View File

@@ -0,0 +1,70 @@
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.http import HttpResponse
from django.template import Template, Context
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection',
'aggregate', 'prefix', 'ipaddress', 'vlan',
'provider', 'circuit'
]
class Graph(models.Model):
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
weight = models.PositiveSmallIntegerField(default=1000)
name = models.CharField(max_length=100, verbose_name='Name')
source = models.CharField(max_length=500, verbose_name='Source URL')
link = models.URLField(verbose_name='Link URL', blank=True)
class Meta:
ordering = ['type', 'weight', 'name']
def __unicode__(self):
return self.name
def embed_url(self, obj):
template = Template(self.source)
return template.render(Context({'obj': obj}))
class ExportTemplate(models.Model):
content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
name = models.CharField(max_length=200)
template_code = models.TextField()
mime_type = models.CharField(max_length=15, blank=True)
file_extension = models.CharField(max_length=15, blank=True)
class Meta:
ordering = ['content_type', 'name']
unique_together = [
['content_type', 'name']
]
def __unicode__(self):
return "{}: {}".format(self.content_type, self.name)
def to_response(self, context_dict, filename):
"""
Render the template to an HTTP response, delivered as a named file attachment
"""
template = Template(self.template_code)
mime_type = 'text/plain' if not self.mime_type else self.mime_type
response = HttpResponse(
template.render(Context(context_dict)),
content_type=mime_type
)
if self.file_extension:
filename += '.{}'.format(self.file_extension)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response

247
netbox/extras/rpc.py Normal file
View File

@@ -0,0 +1,247 @@
from Exscript import Account
from Exscript.protocols import SSH2
from ncclient import manager
import paramiko
import re
import xmltodict
CONNECT_TIMEOUT = 5 # seconds
class RPCClient(object):
def __init__(self, device, username='', password=''):
self.username = username
self.password = password
try:
self.host = str(device.primary_ip.address.ip)
except AttributeError:
raise Exception("Specified device ({}) does not have a primary IP defined.".format(device))
def get_lldp_neighbors(self):
"""
Returns a list of dictionaries, each representing an LLDP neighbor adjacency.
{
'local-interface': <str>,
'name': <str>,
'remote-interface': <str>,
'chassis-id': <str>,
}
"""
raise NotImplementedError("Feature not implemented for this platform.")
def get_inventory(self):
"""
Returns a dictionary representing the device chassis and installed modules.
{
'chassis': {
'serial': <str>,
'description': <str>,
}
'modules': [
{
'name': <str>,
'part_id': <str>,
'serial': <str>,
},
...
]
}
"""
raise NotImplementedError("Feature not implemented for this platform.")
class JunosNC(RPCClient):
"""
NETCONF client for Juniper Junos devices
"""
def __enter__(self):
# Initiate a connection to the device
self.manager = manager.connect(host=self.host, username=self.username, password=self.password,
hostkey_verify=False, timeout=CONNECT_TIMEOUT)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Close the connection to the device
self.manager.close_session()
def get_lldp_neighbors(self):
rpc_reply = self.manager.dispatch('get-lldp-neighbors-information')
lldp_neighbors_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['lldp-neighbors-information']['lldp-neighbor-information']
result = []
for neighbor_raw in lldp_neighbors_raw:
neighbor = dict()
neighbor['local-interface'] = neighbor_raw.get('lldp-local-port-id')
neighbor['name'] = neighbor_raw.get('lldp-remote-system-name')
neighbor['name'] = neighbor['name'].split('.')[0] # Split hostname from domain if one is present
try:
neighbor['remote-interface'] = neighbor_raw['lldp-remote-port-description']
except KeyError:
# Older versions of Junos report on interface ID instead of description
neighbor['remote-interface'] = neighbor_raw.get('lldp-remote-port-id')
neighbor['chassis-id'] = neighbor_raw.get('lldp-remote-chassis-id')
result.append(neighbor)
return result
def get_inventory(self):
rpc_reply = self.manager.dispatch('get-chassis-inventory')
inventory_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['chassis-inventory']['chassis']
result = dict()
# Gather chassis data
result['chassis'] = {
'serial': inventory_raw['serial-number'],
'description': inventory_raw['description'],
}
# Gather modules
result['modules'] = []
for module in inventory_raw['chassis-module']:
try:
# Skip built-in modules
if module['name'] and module['serial-number'] != inventory_raw['serial-number']:
result['modules'].append({
'name': module['name'],
'part_id': module['model-number'] or '',
'serial': module['serial-number'] or '',
})
except KeyError:
pass
return result
class IOSSSH(RPCClient):
"""
SSH client for Cisco IOS devices
"""
def __enter__(self):
# Initiate a connection to the device
self.ssh = SSH2(connect_timeout=CONNECT_TIMEOUT)
self.ssh.connect(self.host)
self.ssh.login(Account(self.username, self.password))
# Disable terminal paging
self.ssh.execute("terminal length 0")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Close the connection to the device
self.ssh.send("exit\r")
self.ssh.close()
def get_inventory(self):
result = dict()
# Gather chassis data
try:
self.ssh.execute("show version")
show_version = self.ssh.response
serial = re.search("Processor board ID ([^\s]+)", show_version).groups()[0]
description = re.search("\r\n\r\ncisco ([^\s]+)", show_version).groups()[0]
except:
raise RuntimeError("Failed to glean chassis info from device.")
result['chassis'] = {
'serial': serial,
'description': description,
}
# Gather modules
result['modules'] = []
try:
self.ssh.execute("show inventory")
show_inventory = self.ssh.response
# Split modules on double line
modules_raw = show_inventory.strip().split('\r\n\r\n')
for module_raw in modules_raw:
try:
m_name = re.search('NAME: "([^"]+)"', module_raw).group(1)
m_pid = re.search('PID: ([^\s]+)', module_raw).group(1)
m_serial = re.search('SN: ([^\s]+)', module_raw).group(1)
# Omit built-in modules and those with no PID
if m_serial != result['chassis']['serial'] and m_pid.lower() != 'unspecified':
result['modules'].append({
'name': m_name,
'part_id': m_pid,
'serial': m_serial,
})
except AttributeError:
continue
except:
raise RuntimeError("Failed to glean module info from device.")
return result
class OpengearSSH(RPCClient):
"""
SSH client for Opengear devices
"""
def __enter__(self):
# Initiate a connection to the device
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
self.ssh.connect(self.host, username=self.username, password=self.password, timeout=CONNECT_TIMEOUT)
except paramiko.AuthenticationException:
# Try default Opengear credentials if the configured creds don't work
self.ssh.connect(self.host, username='root', password='default')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Close the connection to the device
self.ssh.close()
def get_inventory(self):
try:
stdin, stdout, stderr = self.ssh.exec_command("showserial")
serial = stdout.readlines()[0].strip()
except:
raise RuntimeError("Failed to glean chassis serial from device.")
# Older models don't provide serial info
if serial == "No serial number information available":
serial = ''
try:
stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
description = stdout.readlines()[0].split(' ', 1)[1].strip()
except:
raise RuntimeError("Failed to glean chassis description from device.")
return {
'chassis': {
'serial': serial,
'description': description,
},
'modules': [],
}
# For mapping platform -> NC client
RPC_CLIENTS = {
'juniper-junos': JunosNC,
'cisco-ios': IOSSSH,
'opengear': OpengearSSH,
}

3
netbox/extras/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
netbox/extras/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.