From f6a8d32880f211b878f18ec68f4e76dc9be5685f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Jul 2017 14:42:56 -0400 Subject: [PATCH] Initial work on NAPALM integration --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/api/views.py | 54 +++++++++++++++++++ netbox/dcim/forms.py | 2 +- .../migrations/0041_napalm_integration.py | 40 ++++++++++++++ netbox/dcim/models.py | 9 +++- 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 netbox/dcim/migrations/0041_napalm_integration.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 39381fc9a..09da3ced7 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -422,7 +422,7 @@ class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = Platform - fields = ['id', 'name', 'slug', 'rpc_client'] + fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client'] class NestedPlatformSerializer(serializers.ModelSerializer): diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 8c888e60f..64733de5d 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet from django.conf import settings +from django.http import Http404, HttpResponseForbidden from django.shortcuts import get_object_or_404 from dcim.models import ( @@ -224,6 +225,59 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): write_serializer_class = serializers.WritableDeviceSerializer filter_class = filters.DeviceFilter + @detail_route(url_path='napalm/(?Pget_[a-z_]+)') + def napalm(self, request, pk, method): + """ + Execute a NAPALM method on a Device + """ + device = get_object_or_404(Device, pk=pk) + if not device.primary_ip: + raise ServiceUnavailable("This device does not have a primary IP address configured.") + if device.platform is None: + raise ServiceUnavailable("No platform is configured for this device.") + if not device.platform.napalm_driver: + raise ServiceUnavailable("No NAPALM driver is configured for this device's platform ().".format( + device.platform + )) + + # Check that NAPALM is installed and verify the configured driver + try: + import napalm + from napalm_base.exceptions import ConnectAuthError, ModuleImportError + except ImportError: + raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.") + try: + driver = napalm.get_network_driver(device.platform.napalm_driver) + except ModuleImportError: + raise ServiceUnavailable("NAPALM driver for platform {} not found: {}.".format( + device.platform, device.platform.napalm_driver + )) + + # Raise a 404 for invalid NAPALM methods + if not hasattr(driver, method): + raise Http404() + + # Verify user permission + if not request.user.has_perm('dcim.napalm_read'): + return HttpResponseForbidden() + + # Connect to the device and execute the given method + # TODO: Improve error handling + ip_address = str(device.primary_ip.address.ip) + d = driver( + hostname=ip_address, + username=settings.NETBOX_USERNAME, + password=settings.NETBOX_PASSWORD + ) + try: + d.open() + response = getattr(d, method)() + except Exception as e: + raise ServiceUnavailable("Error connecting to the device: {}".format(e)) + + return Response(response) + + @detail_route(url_path='lldp-neighbors') def lldp_neighbors(self, request, pk): """ diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 740a9c9a6..440c12623 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -558,7 +558,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm): class Meta: model = Platform - fields = ['name', 'slug', 'rpc_client'] + fields = ['name', 'slug', 'napalm_driver', 'rpc_client'] # diff --git a/netbox/dcim/migrations/0041_napalm_integration.py b/netbox/dcim/migrations/0041_napalm_integration.py new file mode 100644 index 000000000..73ca8f3ee --- /dev/null +++ b/netbox/dcim/migrations/0041_napalm_integration.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-07-14 17:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def rpc_client_to_napalm_driver(apps, schema_editor): + """ + Migrate legacy RPC clients to their respective NAPALM drivers + """ + Platform = apps.get_model('dcim', 'Platform') + + Platform.objects.filter(rpc_client='juniper-junos').update(napalm_driver='junos') + Platform.objects.filter(rpc_client='cisco-ios').update(napalm_driver='ios') + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0040_inventoryitem_add_asset_tag_description'), + ] + + operations = [ + migrations.AlterModelOptions( + name='device', + options={'ordering': ['name'], 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))}, + ), + migrations.AddField( + model_name='platform', + name='napalm_driver', + field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices.', max_length=50, verbose_name='NAPALM driver'), + ), + migrations.AlterField( + model_name='platform', + name='rpc_client', + field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='Legacy RPC client'), + ), + migrations.RunPython(rpc_client_to_napalm_driver), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index f1506a924..8dd11e663 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -738,7 +738,10 @@ class Platform(models.Model): """ name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) - rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, verbose_name='RPC client') + napalm_driver = models.CharField(max_length=50, blank=True, verbose_name='NAPALM driver', + help_text="The name of the NAPALM driver to use when interacting with devices.") + rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, + verbose_name='Legacy RPC client') class Meta: ordering = ['name'] @@ -809,6 +812,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel): class Meta: ordering = ['name'] unique_together = ['rack', 'position', 'face'] + permissions = ( + ('napalm_read', 'Read-only access to devices via NAPALM'), + ('napalm_write', 'Read/write access to devices via NAPALM'), + ) def __str__(self): return self.display_name or super(Device, self).__str__()