Repository: netbox-community/netbox

Issue:
Topology view with overlapping regexps
### Issue type
[ ] Feature request <!-- Requesting the implementation of a new feature -->
[X] Bug report      <!-- Reporting unexpected or erroneous behavior -->
[ ] Documentation   <!-- Proposing a modification to the documentation -->

### Environment
* Python version:  3.5.2
* NetBox version:  v2.2-beta1

### Description

When I enter a topology description like this:

~~~
sw
.
~~~

I was expecting to see all devices with "sw" in their name on the top line, and all other devices on the line below.

What I actually see is all devices on the same line.

It's not entirely clear what the intended behaviour is here. What I was hoping is that if a device matches an earlier regex, it should be excluded from matching a later one.

If I change this to:

~~~
.
sw
~~~

then I also get all the devices on the same line - but in this case, the 'sw' device is on the right-hand end of the line.

If I make a regex which explicitly matches the other hosts by name (i.e. all those without 'sw' in them), it works - but it can be long and hard to maintain.


Discussion:
> I was expecting to see all devices with "sw" in their name on the top line, and all other devices on the line below.

Each line is evaluated as an independent regex (or set of regexes separated by semicolons). You should be getting two rows: the first with all devices with "sw" in their name, and the second with _all_ devices.

> If I make a regex which explicitly matches the other hosts by name (i.e. all those without 'sw' in them), it works - but it can be long and hard to maintain.

Try:

```
sw
^((?!sw).)*$
```
> Each line is evaluated as an independent regex (or set of regexes separated by semicolons). You should be getting two rows: the first with all devices with "sw" in their name, and the second with all devices.

That doesn't happen. And

~~~
.
.
~~~

just puts one instance of each device, all on the same line.

OK, I found the issue.  If you repeat a node (with the same name) in graphviz, then it's not added as another node - it's just a repeat instance of the same node.

This fixes it for me:

~~~
--- a/netbox/extras/models.py
+++ b/netbox/extras/models.py
@@ -273,6 +273,7 @@ class TopologyMap(models.Model):
         # Construct the graph
         graph = graphviz.Graph()
         graph.graph_attr['ranksep'] = '1'
+        seen = set()
         for i, device_set in enumerate(self.device_sets):

             subgraph = graphviz.Graph(name='sg{}'.format(i))
@@ -287,7 +288,10 @@ class TopologyMap(models.Model):
             devices = []
             for query in device_set.strip(';').split(';'):  # Split regexes on semicolons
                 devices += Device.objects.filter(name__regex=query).select_related('device_role')
+            # Exclude nodes which have already been added
+            devices = [d for d in devices if d.id not in seen]
             for d in devices:
+                seen.add(d.id)
                 bg_color = '#{}'.format(d.device_role.color)
                 fg_color = '#{}'.format(foreground_color(d.device_role.color))
                 subgraph.node(d.name, style='filled', fillcolor=bg_color, fontcolor=fg_color, fontname='sans')
~~~

Or if you prefer stylistically:

~~~
--- a/netbox/extras/models.py
+++ b/netbox/extras/models.py
@@ -273,6 +273,7 @@ class TopologyMap(models.Model):
         # Construct the graph
         graph = graphviz.Graph()
         graph.graph_attr['ranksep'] = '1'
+        seen = set()
         for i, device_set in enumerate(self.device_sets):

             subgraph = graphviz.Graph(name='sg{}'.format(i))
@@ -287,6 +288,9 @@ class TopologyMap(models.Model):
             devices = []
             for query in device_set.strip(';').split(';'):  # Split regexes on semicolons
                 devices += Device.objects.filter(name__regex=query).select_related('device_role')
+            # Remove duplicate devices
+            devices = [d for d in devices if d.id not in seen]
+            seen.update([d.id for d in devices])
             for d in devices:
                 bg_color = '#{}'.format(d.device_role.color)
                 fg_color = '#{}'.format(foreground_color(d.device_role.color))
~~~


Resolving patch:
diff --git a/netbox/extras/models.py b/netbox/extras/models.py
--- a/netbox/extras/models.py
+++ b/netbox/extras/models.py
@@ -274,6 +274,7 @@ def render(self, img_format='png'):
         # Construct the graph
         graph = graphviz.Graph()
         graph.graph_attr['ranksep'] = '1'
+        seen = set()
         for i, device_set in enumerate(self.device_sets):
 
             subgraph = graphviz.Graph(name='sg{}'.format(i))
@@ -288,6 +289,9 @@ def render(self, img_format='png'):
             devices = []
             for query in device_set.strip(';').split(';'):  # Split regexes on semicolons
                 devices += Device.objects.filter(name__regex=query).select_related('device_role')
+            # Remove duplicate devices
+            devices = [d for d in devices if d.id not in seen]
+            seen.update([d.id for d in devices])
             for d in devices:
                 bg_color = '#{}'.format(d.device_role.color)
                 fg_color = '#{}'.format(foreground_color(d.device_role.color))


Test patch: