diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index e4da0761e..22fd9ee32 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -1,7 +1,7 @@ from django.conf.urls import url from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE -from extras.api.views import GraphListView +from extras.api.views import GraphListView, TopologyMapperView from .views import * @@ -62,5 +62,6 @@ urlpatterns = [ # Miscellaneous url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'), + url(r'^topology-mapper/$', TopologyMapperView.as_view(), name='topology_mapper'), ] diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 01563ecaf..0038bed05 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,10 +1,15 @@ +import pydot from rest_framework import generics +from rest_framework.views import APIView +import tempfile +from wsgiref.util import FileWrapper -from django.http import Http404 +from django.db.models import Q +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 from circuits.models import Provider -from dcim.models import Site, Interface +from dcim.models import Site, Device, Interface, InterfaceConnection from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_PROVIDER, GRAPH_TYPE_SITE from .serializers import GraphSerializer @@ -31,3 +36,60 @@ class GraphListView(generics.ListAPIView): raise Http404() queryset = Graph.objects.filter(type=graph_type) return queryset + + +class TopologyMapperView(APIView): + """ + Generate a topology diagram + """ + + def get(self, request): + + # Glean device sets to map. Each set is represented as a hierarchical tier in the diagram. + device_sets = request.GET.getlist('devices', []) + + # Construct the graph + graph = pydot.Dot(graph_type='graph', ranksep='1') + for i, device_set in enumerate(device_sets): + + subgraph = pydot.Subgraph('sg{}'.format(i), rank='same') + + # Add a pseudonode for each device_set to enforce hierarchical layout + subgraph.add_node(pydot.Node('set{}'.format(i), shape='none')) + if i: + graph.add_edge(pydot.Edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')) + + # Add each device to the graph + devices = Device.objects.filter(name__regex=device_set) + for d in devices: + node = pydot.Node(d.name) + subgraph.add_node(node) + + # Add an invisible connection to each successive device in a set to enforce horizontal order + for j in range(0, len(devices) - 1): + edge = pydot.Edge(devices[j].name, devices[j + 1].name) + # edge.set('style', 'invis') doesn't seem to work for some reason + edge.set_style('invis') + subgraph.add_edge(edge) + + graph.add_subgraph(subgraph) + + # Compile list of all devices + device_superset = Q() + for regex in device_sets: + device_superset = device_superset | Q(name__regex=regex) + + # 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: + edge = pydot.Edge(c.interface_a.device.name, c.interface_b.device.name) + graph.add_edge(edge) + + # Write the image to disk and return + topo_file = tempfile.NamedTemporaryFile() + graph.write(topo_file.name, format='png') + response = HttpResponse(FileWrapper(topo_file), content_type='image/png') + topo_file.close() + + return response