From 12472a2612d8d6b0b290224f153131e2d1659bc9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Jul 2017 16:07:28 -0400 Subject: [PATCH] Live device status PoC --- netbox/dcim/api/views.py | 26 +++++--- netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 21 ++++++ netbox/templates/dcim/device_status.html | 67 ++++++++++++++++++++ netbox/templates/dcim/inc/device_header.html | 1 + 5 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 netbox/templates/dcim/device_status.html diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 64733de5d..8275aa888 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from collections import OrderedDict from rest_framework.decorators import detail_route from rest_framework.mixins import ListModelMixin @@ -7,7 +8,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.http import HttpResponseBadRequest, HttpResponseForbidden from django.shortcuts import get_object_or_404 from dcim.models import ( @@ -225,8 +226,8 @@ 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): + @detail_route(url_path='napalm') + def napalm(self, request, pk): """ Execute a NAPALM method on a Device """ @@ -253,16 +254,21 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): 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 + # Validate requested NAPALM methods + napalm_methods = request.GET.getlist('method') + for method in napalm_methods: + if not hasattr(driver, method): + return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method)) + elif not method.startswith('get_'): + return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method)) + + # Connect to the device and execute the requested methods # TODO: Improve error handling + response = OrderedDict([(m, None) for m in napalm_methods]) ip_address = str(device.primary_ip.address.ip) d = driver( hostname=ip_address, @@ -271,10 +277,12 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): ) try: d.open() - response = getattr(d, method)() + for method in napalm_methods: + response[method] = getattr(d, method)() except Exception as e: raise ServiceUnavailable("Error connecting to the device: {}".format(e)) + d.close() return Response(response) diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 53031ebbe..6adbc4dae 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -122,6 +122,7 @@ urlpatterns = [ url(r'^devices/(?P\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), + url(r'^devices/(?P\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'), url(r'^devices/(?P\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), url(r'^devices/(?P\d+)/add-secret/$', secret_add, name='device_addsecret'), url(r'^devices/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9af1a320c..221916c9f 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -921,6 +921,27 @@ class DeviceInventoryView(View): }) +class DeviceStatusView(View): + + def get(self, request, pk): + + device = get_object_or_404(Device, pk=pk) + method = request.GET.get('method', 'get_facts') + + interfaces = Interface.objects.order_naturally( + device.device_type.interface_ordering + ).filter( + device=device + ).select_related( + 'connected_as_a', 'connected_as_b' + ) + + return render(request, 'dcim/device_status.html', { + 'device': device, + 'interfaces': interfaces, + }) + + class DeviceLLDPNeighborsView(View): def get(self, request, pk): diff --git a/netbox/templates/dcim/device_status.html b/netbox/templates/dcim/device_status.html new file mode 100644 index 000000000..d5931b550 --- /dev/null +++ b/netbox/templates/dcim/device_status.html @@ -0,0 +1,67 @@ +{% extends '_base.html' %} + +{% block title %}{{ device }} - NAPALM{% endblock %} + +{% block content %} + {% include 'dcim/inc/device_header.html' with active_tab='status' %} +
+
+
+
Device Facts
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Hostname
FQDN
Vendor
Model
Serial Number
OS Version
Uptime
+
+
+
+{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/netbox/templates/dcim/inc/device_header.html b/netbox/templates/dcim/inc/device_header.html index 6861abf4a..8a807873a 100644 --- a/netbox/templates/dcim/inc/device_header.html +++ b/netbox/templates/dcim/inc/device_header.html @@ -45,6 +45,7 @@