Browse Source

Closes #78: Implemented ability to render topology maps for console/power

Jeremy Stretch 7 years ago
parent
commit
2d93c2b2da
3 changed files with 90 additions and 12 deletions
  1. 10 0
      netbox/extras/constants.py
  2. 20 0
      netbox/extras/migrations/0009_topologymap_type.py
  3. 60 12
      netbox/extras/models.py

+ 10 - 0
netbox/extras/constants.py

@@ -46,6 +46,16 @@ EXPORTTEMPLATE_MODELS = [
     'cluster', 'virtualmachine',                                                    # Virtualization
 ]
 
+# Topology map types
+TOPOLOGYMAP_TYPE_NETWORK = 1
+TOPOLOGYMAP_TYPE_CONSOLE = 2
+TOPOLOGYMAP_TYPE_POWER = 3
+TOPOLOGYMAP_TYPE_CHOICES = (
+    (TOPOLOGYMAP_TYPE_NETWORK, 'Network'),
+    (TOPOLOGYMAP_TYPE_CONSOLE, 'Console'),
+    (TOPOLOGYMAP_TYPE_POWER, 'Power'),
+)
+
 # User action types
 ACTION_CREATE = 1
 ACTION_IMPORT = 2

+ 20 - 0
netbox/extras/migrations/0009_topologymap_type.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.9 on 2018-02-15 16:28
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0008_reports'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='topologymap',
+            name='type',
+            field=models.PositiveSmallIntegerField(choices=[(1, 'Network'), (2, 'Console'), (3, 'Power')], default=1),
+        ),
+    ]

+ 60 - 12
netbox/extras/models.py

@@ -16,6 +16,7 @@ from django.template import Template, Context
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.safestring import mark_safe
 
+from dcim.constants import CONNECTION_STATUS_CONNECTED
 from utilities.utils import foreground_color
 from .constants import *
 
@@ -253,7 +254,17 @@ class ExportTemplate(models.Model):
 class TopologyMap(models.Model):
     name = models.CharField(max_length=50, unique=True)
     slug = models.SlugField(unique=True)
-    site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True, on_delete=models.CASCADE)
+    type = models.PositiveSmallIntegerField(
+        choices=TOPOLOGYMAP_TYPE_CHOICES,
+        default=TOPOLOGYMAP_TYPE_NETWORK
+    )
+    site = models.ForeignKey(
+        to='dcim.Site',
+        related_name='topology_maps',
+        blank=True,
+        null=True,
+        on_delete=models.CASCADE
+    )
     device_patterns = models.TextField(
         help_text="Identify devices to include in the diagram using regular expressions, one per line. Each line will "
                   "result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. "
@@ -275,22 +286,26 @@ class TopologyMap(models.Model):
 
     def render(self, img_format='png'):
 
-        from circuits.models import CircuitTermination
-        from dcim.models import CONNECTION_STATUS_CONNECTED, Device, InterfaceConnection
+        from dcim.models import Device
 
         # Construct the graph
-        graph = graphviz.Graph()
-        graph.graph_attr['ranksep'] = '1'
+        if self.type == TOPOLOGYMAP_TYPE_NETWORK:
+            G = graphviz.Graph
+        else:
+            G = graphviz.Digraph
+        self.graph = G()
+        self.graph.graph_attr['ranksep'] = '1'
         seen = set()
         for i, device_set in enumerate(self.device_sets):
 
-            subgraph = graphviz.Graph(name='sg{}'.format(i))
+            subgraph = G(name='sg{}'.format(i))
             subgraph.graph_attr['rank'] = 'same'
+            subgraph.graph_attr['directed'] = 'true'
 
             # 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')
+                self.graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
 
             # Add each device to the graph
             devices = []
@@ -308,31 +323,64 @@ class TopologyMap(models.Model):
             for j in range(0, len(devices) - 1):
                 subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
 
-            graph.subgraph(subgraph)
+            self.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)
+        devices = Device.objects.filter(*(device_superset,))
+
+        # Draw edges depending on graph type
+        if self.type == TOPOLOGYMAP_TYPE_NETWORK:
+            self.add_network_connections(devices)
+        elif self.type == TOPOLOGYMAP_TYPE_CONSOLE:
+            self.add_console_connections(devices)
+        elif self.type == TOPOLOGYMAP_TYPE_POWER:
+            self.add_power_connections(devices)
+
+        return self.graph.pipe(format=img_format)
+
+    def add_network_connections(self, devices):
+
+        from circuits.models import CircuitTermination
+        from dcim.models import InterfaceConnection
 
         # Add all interface 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:
             style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
-            graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
+            self.graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
 
         # Add all circuits to the graph
         for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
             peer_termination = termination.get_peer_termination()
             if (peer_termination is not None and peer_termination.interface is not None and
                     peer_termination.interface.device in devices):
-                graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
+                self.graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
+
+    def add_console_connections(self, devices):
+
+        from dcim.models import ConsolePort
+
+        # Add all console connections to the graph
+        console_ports = ConsolePort.objects.filter(device__in=devices, cs_port__device__in=devices)
+        for cp in console_ports:
+            style = 'solid' if cp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
+            self.graph.edge(cp.cs_port.device.name, cp.device.name, style=style)
+
+    def add_power_connections(self, devices):
+
+        from dcim.models import PowerPort
 
-        return graph.pipe(format=img_format)
+        # Add all power connections to the graph
+        power_ports = PowerPort.objects.filter(device__in=devices, power_outlet__device__in=devices)
+        for pp in power_ports:
+            style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
+            self.graph.edge(pp.power_outlet.device.name, pp.device.name, style=style)
 
 
 #