Skip to content

Commit

Permalink
update to use new APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
arthanson committed Feb 16, 2023
1 parent 7fa343f commit 614b326
Show file tree
Hide file tree
Showing 17 changed files with 299 additions and 93 deletions.
14 changes: 7 additions & 7 deletions netbox_napalm_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""Top-level package for NetBox Napalm Plugin."""

__author__ = """Arthur Hanson"""
__email__ = '[email protected]'
__version__ = '0.1.0'
__email__ = "[email protected]"
__version__ = "0.1.0"


from extras.plugins import PluginConfig


class NapalmConfig(PluginConfig):
name = 'netbox_napalm_plugin'
verbose_name = 'NetBox Napalm Plugin'
description = 'NetBox plugin for Napalm.'
version = 'version'
base_url = 'netbox_napalm_plugin'
name = "netbox_napalm_plugin"
verbose_name = "NetBox Napalm Plugin"
description = "NetBox plugin for Napalm."
version = "version"
base_url = "netbox_napalm_plugin"


config = NapalmConfig
28 changes: 28 additions & 0 deletions netbox_napalm_plugin/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from dcim.api.serializers import NestedPlatformSerializer
from netbox.api.serializers import NetBoxModelSerializer
from rest_framework import serializers

from netbox_napalm_plugin.models import NapalmPlatform


class NapalmPlatformSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_napalm_plugin:napalmplatform-detail"
)
platform = NestedPlatformSerializer()

class Meta:
model = NapalmPlatform
fields = [
"id",
"platform",
"napalm_driver",
"napalm_args",
"tags",
"created",
"last_updated",
]


class DeviceNAPALMSerializer(serializers.Serializer):
method = serializers.JSONField()
8 changes: 8 additions & 0 deletions netbox_napalm_plugin/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# api/urls.py
from netbox.api.routers import NetBoxRouter

from .views import NapalmPlatformViewSet

router = NetBoxRouter()
router.register("napalmplatform", NapalmPlatformViewSet)
urlpatterns = router.urls
147 changes: 147 additions & 0 deletions netbox_napalm_plugin/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from dcim.models import Device
from django.shortcuts import get_object_or_404, redirect, render
from netbox.api.pagination import StripCountAnnotationsPaginator
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.config import get_config
from rest_framework.decorators import action
from rest_framework.response import Response

from netbox_napalm_plugin import filtersets
from netbox_napalm_plugin.models import NapalmPlatform

from . import serializers


class NapalmPlatformViewSet(NetBoxModelViewSet):
queryset = NapalmPlatform.objects.prefetch_related(
"platform",
"tags",
)
serializer_class = serializers.NapalmPlatformSerializer
filterset_class = filtersets.NapalmPlatformFilterSet
pagination_class = StripCountAnnotationsPaginator

@action(detail=True, url_path="napalm")
def napalm(self, request, pk):
"""
Execute a NAPALM method on a Device
"""
device = get_object_or_404(Device.objects.all(), 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 hasattr(device.platform, "napalm")
or not device.platform.napalm.napalm_driver
):
raise ServiceUnavailable(
f"No NAPALM driver is configured for this device's platform: {device.platform}."
)

# Check for primary IP address from NetBox object
if device.primary_ip:
host = str(device.primary_ip.address.ip)
else:
# Raise exception for no IP address and no Name if device.name does not exist
if not device.name:
raise ServiceUnavailable(
"This device does not have a primary IP address or device name to lookup configured."
)
try:
# Attempt to complete a DNS name resolution if no primary_ip is set
host = socket.gethostbyname(device.name)
except socket.gaierror:
# Name lookup failure
raise ServiceUnavailable(
f"Name lookup failure, unable to resolve IP address for {device.name}. Please set Primary IP or "
f"setup name resolution."
)

# Check that NAPALM is installed
try:
import napalm
from napalm.base.exceptions import ModuleImportError
except ModuleNotFoundError as e:
if getattr(e, "name") == "napalm":
raise ServiceUnavailable(
"NAPALM is not installed. Please see the documentation for instructions."
)
raise e

# Validate the configured driver
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
)
)

# Verify user permission
if not request.user.has_perm("dcim.napalm_read_device"):
return HttpResponseForbidden()

napalm_methods = request.GET.getlist("method")
response = {m: None for m in napalm_methods}

config = get_config()
username = config.NAPALM_USERNAME
password = config.NAPALM_PASSWORD
timeout = config.NAPALM_TIMEOUT
optional_args = config.NAPALM_ARGS.copy()
if device.platform.napalm_args is not None:
optional_args.update(device.platform.napalm_args)

# Update NAPALM parameters according to the request headers
for header in request.headers:
if header[:9].lower() != "x-napalm-":
continue

key = header[9:]
if key.lower() == "username":
username = request.headers[header]
elif key.lower() == "password":
password = request.headers[header]
elif key:
optional_args[key.lower()] = request.headers[header]

# Connect to the device
d = driver(
hostname=host,
username=username,
password=password,
timeout=timeout,
optional_args=optional_args,
)
try:
d.open()
except Exception as e:
raise ServiceUnavailable(
"Error connecting to the device at {}: {}".format(host, e)
)

# Validate and execute each specified NAPALM method
for method in napalm_methods:
if not hasattr(driver, method):
response[method] = {"error": "Unknown NAPALM method"}
continue
if not method.startswith("get_"):
response[method] = {"error": "Only get_* NAPALM methods are supported"}
continue
try:
response[method] = getattr(d, method)()
except NotImplementedError:
response[method] = {
"error": "Method {} not implemented for NAPALM driver {}".format(
method, driver
)
}
except Exception as e:
response[method] = {"error": "Method {} failed: {}".format(method, e)}
d.close()

return Response(response)
31 changes: 22 additions & 9 deletions netbox_napalm_plugin/filtersets.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
from netbox.filtersets import NetBoxModelFilterSet
import django_filters
from dcim.models import Platform
from django.utils.translation import gettext as _
from netbox.filtersets import (NetBoxModelFilterSet,
OrganizationalModelFilterSet)

from .models import NapalmPlatform


# class NapalmFilterSet(NetBoxModelFilterSet):
#
# class Meta:
# model = Napalm
# fields = ['name', ]
#
# def search(self, queryset, name, value):
# return queryset.filter(description__icontains=value)
class NapalmPlatformFilterSet(NetBoxModelFilterSet):
platform_id = django_filters.ModelMultipleChoiceFilter(
field_name="platform",
queryset=Platform.objects.all(),
label=_("Platform (ID)"),
)
platform = django_filters.ModelMultipleChoiceFilter(
field_name="platform__slug",
queryset=Platform.objects.all(),
to_field_name="slug",
label=_("Platform (slug)"),
)

class Meta:
model = NapalmPlatform
fields = ["id", "napalm_driver"]
12 changes: 8 additions & 4 deletions netbox_napalm_plugin/forms.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from django import forms

from ipam.models import Prefix
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField

from .models import NapalmPlatform


class NapalmPlatformForm(NetBoxModelForm):

class Meta:
model = NapalmPlatform
fields = ('tags', )
fields = (
"platform",
"napalm_driver",
"napalm_args",
"tags",
)
2 changes: 1 addition & 1 deletion netbox_napalm_plugin/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Generated by Django 4.1.5 on 2023-02-15 16:57

from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
from django.db import migrations, models


class Migration(migrations.Migration):
Expand Down
8 changes: 5 additions & 3 deletions netbox_napalm_plugin/migrations/0002_auto_20230215_1752.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

from django.db import migrations


def migrate_napalm(apps, schema_editor):
Platform = apps.get_model('dcim', 'Platform')
NapalmPlatform = apps.get_model('netbox_napalm_plugin', 'NapalmPlatform')
qs = Platform.objects.all().exclude(napalm_driver__exact='')
Platform = apps.get_model("dcim", "Platform")
NapalmPlatform = apps.get_model("netbox_napalm_plugin", "NapalmPlatform")
qs = Platform.objects.all().exclude(napalm_driver__exact="")
for platform in qs:
NapalmPlatform.objects.create(
platform=platform,
napalm_driver=platform.napalm_driver,
napalm_args=platform.napalm_args,
)


class Migration(migrations.Migration):
dependencies = [
("netbox_napalm_plugin", "0001_initial"),
Expand Down
19 changes: 11 additions & 8 deletions netbox_napalm_plugin/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from dcim.models.devices import Platform
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext as _

from netbox.models import NetBoxModel
from dcim.models.devices import Platform


class NapalmPlatform(NetBoxModel):
Expand All @@ -15,21 +14,25 @@ class NapalmPlatform(NetBoxModel):
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')
verbose_name="NAPALM driver",
help_text=_(
"The name of the NAPALM driver to use when interacting with devices"
),
)
napalm_args = models.JSONField(
blank=True,
null=True,
verbose_name='NAPALM arguments',
help_text=_('Additional arguments to pass when initiating the NAPALM driver (JSON format)')
verbose_name="NAPALM arguments",
help_text=_(
"Additional arguments to pass when initiating the NAPALM driver (JSON format)"
),
)

class Meta:
ordering = ('pk',)
ordering = ("pk",)

def __str__(self):
return f"{self.platform.name} -> {self.napalm_driver}"

def get_absolute_url(self):
return reverse('plugins:netbox_napalm_plugin:napalm', args=[self.pk])
return reverse("plugins:netbox_napalm_plugin:napalm", args=[self.pk])
15 changes: 7 additions & 8 deletions netbox_napalm_plugin/navigation.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices


plugin_buttons = [
PluginMenuButton(
link='plugins:netbox_napalm_plugin:napalmplatform_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN
link="plugins:netbox_napalm_plugin:napalmplatform_add",
title="Add",
icon_class="mdi mdi-plus-thick",
color=ButtonColorChoices.GREEN,
)
]

menu_items = (
PluginMenuItem(
link='plugins:netbox_napalm_plugin:napalmplatform_list',
link_text='Napalm',
buttons=plugin_buttons
link="plugins:netbox_napalm_plugin:napalmplatform_list",
link_text="Napalm",
buttons=plugin_buttons,
),
)
7 changes: 3 additions & 4 deletions netbox_napalm_plugin/tables.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import django_tables2 as tables
from netbox.tables import ChoiceFieldColumn, NetBoxTable

from netbox.tables import NetBoxTable, ChoiceFieldColumn
from .models import NapalmPlatform


class NapalmPlatformTable(NetBoxTable):

class Meta(NetBoxTable.Meta):
model = NapalmPlatform
fields = ('pk', 'platform__name', 'napalm_driver', 'napalm_args', 'actions')
default_columns = ('platform__name', 'napalm_driver')
fields = ("pk", "platform__name", "napalm_driver", "napalm_args", "actions")
default_columns = ("platform__name", "napalm_driver")
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ <h5 class="card-header">Device Configuration</h5>
{% endblock %}

{% block data %}
<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_config"></span>
<span data-object-url="{% url 'plugins-api:netbox_napalm_plugin-api:napalmplatform-napalm' pk=object.pk %}?method=get_config"></span>
{% endblock %}
Loading

0 comments on commit 614b326

Please sign in to comment.