From f43fbffdf71babb975c806664a7d9bda42e6ce6a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 8 Mar 2017 16:12:14 -0500 Subject: [PATCH] Moved TopologyMaps from DCIM to extras --- netbox/dcim/api/urls.py | 2 - netbox/dcim/api/views.py | 31 +------------ netbox/extras/api/serializers.py | 30 ++++++++++++- netbox/extras/api/urls.py | 16 +++++++ netbox/extras/api/views.py | 76 +++++++++----------------------- netbox/extras/filters.py | 21 ++++++++- netbox/extras/models.py | 68 ++++++++++++++++++++++++++++ netbox/netbox/urls.py | 1 + 8 files changed, 157 insertions(+), 88 deletions(-) create mode 100644 netbox/extras/api/urls.py diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index fce61454b..75c1fc1dc 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -2,7 +2,6 @@ from django.conf.urls import include, url from rest_framework import routers -from extras.api.views import TopologyMapView from ipam.api.views import ServiceViewSet from . import views @@ -55,6 +54,5 @@ urlpatterns = [ # Miscellaneous url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'), - url(r'^topology-maps/(?P[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'), ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 1bf13b57b..70e293265 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -208,7 +208,7 @@ class PlatformViewSet(ModelViewSet): class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Device.objects.select_related( - 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'rack__site', 'parent_bay', + 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay', ).prefetch_related( 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', ) @@ -310,35 +310,6 @@ class InterfaceConnectionViewSet(WritableSerializerMixin, ModelViewSet): write_serializer_class = serializers.WritableInterfaceConnectionSerializer -# -# Live queries -# - -class LLDPNeighborsView(APIView): - """ - Retrieve live LLDP neighbors of a device - """ - - def get(self, request, pk): - - device = get_object_or_404(Device, pk=pk) - if not device.primary_ip: - raise ServiceUnavailable(detail="No IP configured for this device.") - - RPC = device.get_rpc_client() - if not RPC: - raise ServiceUnavailable(detail="No RPC client available for this platform ({}).".format(device.platform)) - - # Connect to device and retrieve inventory info - try: - with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client: - lldp_neighbors = rpc_client.get_lldp_neighbors() - except: - raise ServiceUnavailable(detail="Error connecting to the remote device.") - - return Response(lldp_neighbors) - - # # Miscellaneous # diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index fa7552dc9..8e767fbef 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -2,9 +2,14 @@ from django.contrib.contenttypes.models import ContentType from rest_framework import serializers -from extras.models import CustomField, CustomFieldChoice, Graph +# from dcim.api.serializers import NestedSiteSerializer +from extras.models import CustomField, CustomFieldChoice, Graph, TopologyMap +# +# Custom fields +# + class CustomFieldSerializer(serializers.BaseSerializer): """ Extends ModelSerializer to render any CustomFields and their values associated with an object. @@ -41,6 +46,10 @@ class CustomFieldChoiceSerializer(serializers.ModelSerializer): fields = ['id', 'value'] +# +# Graphs +# + class GraphSerializer(serializers.ModelSerializer): embed_url = serializers.SerializerMethodField() embed_link = serializers.SerializerMethodField() @@ -54,3 +63,22 @@ class GraphSerializer(serializers.ModelSerializer): def get_embed_link(self, obj): return obj.embed_link(self.context['graphed_object']) + + +# +# Topology maps +# + +class TopologyMapSerializer(CustomFieldModelSerializer): + # site = NestedSiteSerializer() + + class Meta: + model = TopologyMap + fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description'] + + +class WritableTopologyMapSerializer(serializers.ModelSerializer): + + class Meta: + model = TopologyMap + fields = ['name', 'slug', 'site', 'device_patterns', 'description'] diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py new file mode 100644 index 000000000..b652abf9e --- /dev/null +++ b/netbox/extras/api/urls.py @@ -0,0 +1,16 @@ +from django.conf.urls import include, url + +from rest_framework import routers + +from . import views + + +router = routers.DefaultRouter() + +router.register(r'topology-maps', views.TopologyMapViewSet) + +urlpatterns = [ + + url(r'', include(router.urls)), + +] diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 74ebf073c..fe4bdc309 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,6 +1,6 @@ import graphviz from rest_framework import generics -from rest_framework.views import APIView +from rest_framework.decorators import detail_route from rest_framework.viewsets import ModelViewSet from django.contrib.contenttypes.models import ContentType @@ -10,9 +10,10 @@ from django.shortcuts import get_object_or_404 from circuits.models import Provider from dcim.models import Site, Device, Interface, InterfaceConnection +from extras import filters from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_PROVIDER, GRAPH_TYPE_SITE - -from .serializers import GraphSerializer +from utilities.api import WritableSerializerMixin +from . import serializers class CustomFieldModelViewSet(ModelViewSet): @@ -49,7 +50,7 @@ class GraphListView(generics.ListAPIView): """ Returns a list of relevant graphs """ - serializer_class = GraphSerializer + serializer_class = serializers.GraphSerializer def get_serializer_context(self): cls = { @@ -72,60 +73,27 @@ class GraphListView(generics.ListAPIView): return queryset -class TopologyMapView(APIView): - """ - Generate a topology diagram - """ +class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet): + queryset = TopologyMap.objects.select_related('site') + serializer_class = serializers.TopologyMapSerializer + write_serializer_class = serializers.WritableTopologyMapSerializer + filter_class = filters.TopologyMapFilter - def get(self, request, slug): + @detail_route() + def render(self, request, pk): - tmap = get_object_or_404(TopologyMap, slug=slug) + tmap = get_object_or_404(TopologyMap, pk=pk) + format = 'png' - # Construct the graph - graph = graphviz.Graph() - graph.graph_attr['ranksep'] = '1' - for i, device_set in enumerate(tmap.device_sets): - - subgraph = graphviz.Graph(name='sg{}'.format(i)) - subgraph.graph_attr['rank'] = 'same' - - # Add a pseudonode for each device_set to enforce hierarchical layout - subgraph.node('set{}'.format(i), label='', shape='none', width='0') - if i: - graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis') - - # Add each device to the graph - devices = [] - for query in device_set.split(';'): # Split regexes on semicolons - devices += Device.objects.filter(name__regex=query) - for d in devices: - subgraph.node(d.name) - - # Add an invisible connection to each successive device in a set to enforce horizontal order - for j in range(0, len(devices) - 1): - subgraph.edge(devices[j].name, devices[j + 1].name, style='invis') - - graph.subgraph(subgraph) - - # Compile list of all devices - device_superset = Q() - for device_set in tmap.device_sets: - for query in device_set.split(';'): # Split regexes on semicolons - device_superset = device_superset | Q(name__regex=query) - - # Add all connections to the graph - devices = Device.objects.filter(*(device_superset,)) - connections = InterfaceConnection.objects.filter(interface_a__device__in=devices, - interface_b__device__in=devices) - for c in connections: - graph.edge(c.interface_a.device.name, c.interface_b.device.name) - - # Get the image data and return try: - topo_data = graph.pipe(format='png') + data = tmap.render(format=format) except: - return HttpResponse("There was an error generating the requested graph. Ensure that the GraphViz " - "executables have been installed correctly.") - response = HttpResponse(topo_data, content_type='image/png') + return HttpResponse( + "There was an error generating the requested graph. Ensure that the GraphViz executables have been " + "installed correctly." + ) + + response = HttpResponse(data, content_type='image/{}'.format(format)) + response['Content-Disposition'] = 'inline; filename="{}.{}"'.format(tmap.slug, format) return response diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index bcd9f175f..98f1a5a1e 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -2,7 +2,8 @@ import django_filters from django.contrib.contenttypes.models import ContentType -from .models import CF_TYPE_SELECT, CustomField +from dcim.models import Site +from .models import CF_TYPE_SELECT, CustomField, TopologyMap class CustomFieldFilter(django_filters.Filter): @@ -44,3 +45,21 @@ class CustomFieldFilterSet(django_filters.FilterSet): custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True) for cf in custom_fields: self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type) + + +class TopologyMapFilter(django_filters.FilterSet): + site_id = django_filters.ModelMultipleChoiceFilter( + name='site', + queryset=Site.objects.all(), + label='Site', + ) + site = django_filters.ModelMultipleChoiceFilter( + name='site__slug', + queryset=Site.objects.all(), + to_field_name='slug', + label='Site (slug)', + ) + + class Meta: + model = TopologyMap + fields = ['name', 'slug'] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index f06d0aa29..f86132f0d 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -1,11 +1,13 @@ from collections import OrderedDict from datetime import date +import graphviz from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.validators import ValidationError from django.db import models +from django.db.models import Q from django.http import HttpResponse from django.template import Template, Context from django.utils.encoding import python_2_unicode_compatible @@ -66,6 +68,10 @@ ACTION_CHOICES = ( ) +# +# Custom fields +# + class CustomFieldModel(object): def cf(self): @@ -211,6 +217,10 @@ class CustomFieldChoice(models.Model): CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete() +# +# Graphs +# + @python_2_unicode_compatible class Graph(models.Model): type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES) @@ -236,6 +246,10 @@ class Graph(models.Model): return template.render(Context({'obj': obj})) +# +# Export templates +# + @python_2_unicode_compatible class ExportTemplate(models.Model): content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}) @@ -270,6 +284,10 @@ class ExportTemplate(models.Model): return response +# +# Topology maps +# + @python_2_unicode_compatible class TopologyMap(models.Model): name = models.CharField(max_length=50, unique=True) @@ -294,6 +312,56 @@ class TopologyMap(models.Model): return None return [line.strip() for line in self.device_patterns.split('\n')] + def render(self, format='png'): + + from dcim.models import Device, InterfaceConnection + + # Construct the graph + graph = graphviz.Graph() + graph.graph_attr['ranksep'] = '1' + for i, device_set in enumerate(self.device_sets): + + subgraph = graphviz.Graph(name='sg{}'.format(i)) + subgraph.graph_attr['rank'] = 'same' + + # Add a pseudonode for each device_set to enforce hierarchical layout + subgraph.node('set{}'.format(i), label='', shape='none', width='0') + if i: + graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis') + + # Add each device to the graph + devices = [] + for query in device_set.split(';'): # Split regexes on semicolons + devices += Device.objects.filter(name__regex=query) + for d in devices: + subgraph.node(d.name) + + # Add an invisible connection to each successive device in a set to enforce horizontal order + for j in range(0, len(devices) - 1): + subgraph.edge(devices[j].name, devices[j + 1].name, style='invis') + + graph.subgraph(subgraph) + + # Compile list of all devices + device_superset = Q() + for device_set in self.device_sets: + for query in device_set.split(';'): # Split regexes on semicolons + device_superset = device_superset | Q(name__regex=query) + + # Add all connections to the graph + devices = Device.objects.filter(*(device_superset,)) + connections = InterfaceConnection.objects.filter( + interface_a__device__in=devices, interface_b__device__in=devices + ) + for c in connections: + graph.edge(c.interface_a.device.name, c.interface_b.device.name) + + return graph.pipe(format=format) + + +# +# User actions +# class UserActionManager(models.Manager): diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index eb6b67fe7..e48c371a2 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -28,6 +28,7 @@ _patterns = [ # API url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')), url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')), + url(r'^api/extras/', include('extras.api.urls', namespace='extras-api')), url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')), url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')), url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),