Просмотр исходного кода

Moved TopologyMaps from DCIM to extras

Jeremy Stretch 9 лет назад
Родитель
Сommit
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 rest_framework import routers
 
 
-from extras.api.views import TopologyMapView
 from ipam.api.views import ServiceViewSet
 from ipam.api.views import ServiceViewSet
 from . import views
 from . import views
 
 
@@ -55,6 +54,5 @@ urlpatterns = [
 
 
     # Miscellaneous
     # Miscellaneous
     url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'),
     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):
 class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
     queryset = Device.objects.select_related(
     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(
     ).prefetch_related(
         'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
         'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
     )
     )
@@ -310,35 +310,6 @@ class InterfaceConnectionViewSet(WritableSerializerMixin, ModelViewSet):
     write_serializer_class = serializers.WritableInterfaceConnectionSerializer
     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
 # 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 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):
 class CustomFieldSerializer(serializers.BaseSerializer):
     """
     """
     Extends ModelSerializer to render any CustomFields and their values associated with an object.
     Extends ModelSerializer to render any CustomFields and their values associated with an object.
@@ -41,6 +46,10 @@ class CustomFieldChoiceSerializer(serializers.ModelSerializer):
         fields = ['id', 'value']
         fields = ['id', 'value']
 
 
 
 
+#
+# Graphs
+#
+
 class GraphSerializer(serializers.ModelSerializer):
 class GraphSerializer(serializers.ModelSerializer):
     embed_url = serializers.SerializerMethodField()
     embed_url = serializers.SerializerMethodField()
     embed_link = serializers.SerializerMethodField()
     embed_link = serializers.SerializerMethodField()
@@ -54,3 +63,22 @@ class GraphSerializer(serializers.ModelSerializer):
 
 
     def get_embed_link(self, obj):
     def get_embed_link(self, obj):
         return obj.embed_link(self.context['graphed_object'])
         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
 import graphviz
 from rest_framework import generics
 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 rest_framework.viewsets import ModelViewSet
 
 
 from django.contrib.contenttypes.models import ContentType
 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 circuits.models import Provider
 from dcim.models import Site, Device, Interface, InterfaceConnection
 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 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):
 class CustomFieldModelViewSet(ModelViewSet):
@@ -49,7 +50,7 @@ class GraphListView(generics.ListAPIView):
     """
     """
     Returns a list of relevant graphs
     Returns a list of relevant graphs
     """
     """
-    serializer_class = GraphSerializer
+    serializer_class = serializers.GraphSerializer
 
 
     def get_serializer_context(self):
     def get_serializer_context(self):
         cls = {
         cls = {
@@ -72,60 +73,27 @@ class GraphListView(generics.ListAPIView):
         return queryset
         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:
         try:
-            topo_data = graph.pipe(format='png')
+            data = tmap.render(format=format)
         except:
         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
         return response

+ 20 - 1
netbox/extras/filters.py

@@ -2,7 +2,8 @@ import django_filters
 
 
 from django.contrib.contenttypes.models import ContentType
 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):
 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)
         custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
         for cf in custom_fields:
         for cf in custom_fields:
             self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type)
             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 collections import OrderedDict
 from datetime import date
 from datetime import date
+import graphviz
 
 
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.core.validators import ValidationError
 from django.core.validators import ValidationError
 from django.db import models
 from django.db import models
+from django.db.models import Q
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.template import Template, Context
 from django.template import Template, Context
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
@@ -66,6 +68,10 @@ ACTION_CHOICES = (
 )
 )
 
 
 
 
+#
+# Custom fields
+#
+
 class CustomFieldModel(object):
 class CustomFieldModel(object):
 
 
     def cf(self):
     def cf(self):
@@ -211,6 +217,10 @@ class CustomFieldChoice(models.Model):
         CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
         CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
 
 
 
 
+#
+# Graphs
+#
+
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class Graph(models.Model):
 class Graph(models.Model):
     type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
     type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
@@ -236,6 +246,10 @@ class Graph(models.Model):
         return template.render(Context({'obj': obj}))
         return template.render(Context({'obj': obj}))
 
 
 
 
+#
+# Export templates
+#
+
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class ExportTemplate(models.Model):
 class ExportTemplate(models.Model):
     content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
     content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
@@ -270,6 +284,10 @@ class ExportTemplate(models.Model):
         return response
         return response
 
 
 
 
+#
+# Topology maps
+#
+
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class TopologyMap(models.Model):
 class TopologyMap(models.Model):
     name = models.CharField(max_length=50, unique=True)
     name = models.CharField(max_length=50, unique=True)
@@ -294,6 +312,56 @@ class TopologyMap(models.Model):
             return None
             return None
         return [line.strip() for line in self.device_patterns.split('\n')]
         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):
 class UserActionManager(models.Manager):
 
 

+ 1 - 0
netbox/netbox/urls.py

@@ -28,6 +28,7 @@ _patterns = [
     # API
     # API
     url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-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/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/ipam/', include('ipam.api.urls', namespace='ipam-api')),
     url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
     url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
     url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
     url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),