diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 808413ba2..d068a3023 100644 --- a/netbox/extras/constants.py +++ b/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 diff --git a/netbox/extras/migrations/0009_topologymap_type.py b/netbox/extras/migrations/0009_topologymap_type.py new file mode 100644 index 000000000..b062c58af --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index aa30f8cdc..678dba6de 100644 --- a/netbox/extras/models.py +++ b/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') - return graph.pipe(format=img_format) + 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 + + # 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) #