Browse Source

Moved TopologyMaps from DCIM to extras

Jeremy Stretch 8 years ago
parent
commit
f43fbffdf7

+ 0 - 2
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<slug>[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'),
 
 ]

+ 1 - 30
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',
     )
@@ -311,35 +311,6 @@ class InterfaceConnectionViewSet(WritableSerializerMixin, ModelViewSet):
 
 
 #
-# 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
 #
 

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

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

+ 22 - 54
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
-    """
-
-    def get(self, request, slug):
-
-        tmap = get_object_or_404(TopologyMap, slug=slug)
-
-        # Construct the graph
-        graph = graphviz.Graph()
-        graph.graph_attr['ranksep'] = '1'
-        for i, device_set in enumerate(tmap.device_sets):
+class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
+    queryset = TopologyMap.objects.select_related('site')
+    serializer_class = serializers.TopologyMapSerializer
+    write_serializer_class = serializers.WritableTopologyMapSerializer
+    filter_class = filters.TopologyMapFilter
 
-            subgraph = graphviz.Graph(name='sg{}'.format(i))
-            subgraph.graph_attr['rank'] = 'same'
+    @detail_route()
+    def render(self, request, pk):
 
-            # 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')
+        tmap = get_object_or_404(TopologyMap, pk=pk)
+        format = 'png'
 
-            # 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

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

+ 68 - 0
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):
 

+ 1 - 0
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')),