Parcourir la source

Initial work on NAPALM integration

Jeremy Stretch il y a 7 ans
Parent
commit
f6a8d32880

+ 1 - 1
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):

+ 54 - 0
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/(?P<method>get_[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):
         """

+ 1 - 1
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']
 
 
 #

+ 40 - 0
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),
+    ]

+ 8 - 1
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__()