فهرست منبع

Closes #2745: Remove topology maps

Jeremy Stretch 6 سال پیش
والد
کامیت
dccda62f2d

+ 0 - 4
base_requirements.txt

@@ -54,10 +54,6 @@ djangorestframework
 # https://github.com/axnsan12/drf-yasg
 drf-yasg[validation]
 
-# Python interface to the graphviz graph rendering utility
-# https://github.com/xflr6/graphviz
-graphviz
-
 # Simple markup language for rendering HTML
 # https://github.com/Python-Markdown/markdown
 # py-gfm requires Markdown<3.0

+ 0 - 17
docs/additional-features/topology-maps.md

@@ -1,17 +0,0 @@
-# Topology Maps
-
-NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
-
-Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure).
-
-To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
-
-Each line of the **device patterns** field represents a hierarchical layer within the topology map. For example, you might map a traditional network with core, distribution, and access tiers like this:
-
-```
-core-switch-[abcd]
-dist-switch\d
-access-switch\d+;oob-switch\d+
-```
-
-Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.

+ 2 - 2
docs/installation/2-netbox.md

@@ -5,14 +5,14 @@ This section of the documentation discusses installing and configuring the NetBo
 **Ubuntu**
 
 ```no-highlight
-# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev redis-server zlib1g-dev
+# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev redis-server zlib1g-dev
 ```
 
 **CentOS**
 
 ```no-highlight
 # yum install -y epel-release
-# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config redis
+# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config redis
 # easy_install-3.6 pip
 # ln -s /usr/bin/python36 /usr/bin/python3
 ```

+ 0 - 1
mkdocs.yml

@@ -32,7 +32,6 @@ pages:
         - Context Data: 'additional-features/context-data.md'
         - Export Templates: 'additional-features/export-templates.md'
         - Graphs: 'additional-features/graphs.md'
-        - Topology Maps: 'additional-features/topology-maps.md'
         - Reports: 'additional-features/reports.md'
         - Webhooks: 'additional-features/webhooks.md'
         - Change Logging: 'additional-features/change-logging.md'

+ 1 - 3
netbox/dcim/views.py

@@ -16,7 +16,7 @@ from django.utils.safestring import mark_safe
 from django.views.generic import View
 
 from circuits.models import Circuit
-from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
+from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
 from extras.views import ObjectConfigContextView
 from ipam.models import Prefix, VLAN
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
@@ -207,14 +207,12 @@ class SiteView(PermissionRequiredMixin, View):
             'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
         }
         rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
-        topology_maps = TopologyMap.objects.filter(site=site)
         show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
 
         return render(request, 'dcim/site.html', {
             'site': site,
             'stats': stats,
             'rack_groups': rack_groups,
-            'topology_maps': topology_maps,
             'show_graphs': show_graphs,
         })
 

+ 1 - 13
netbox/extras/admin.py

@@ -3,7 +3,7 @@ from django.contrib import admin
 
 from netbox.admin import admin_site
 from utilities.forms import LaxURLField
-from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook
+from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, Webhook
 
 
 def order_content_types(field):
@@ -137,15 +137,3 @@ class ExportTemplateForm(forms.ModelForm):
 class ExportTemplateAdmin(admin.ModelAdmin):
     list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
     form = ExportTemplateForm
-
-
-#
-# Topology maps
-#
-
-@admin.register(TopologyMap, site=admin_site)
-class TopologyMapAdmin(admin.ModelAdmin):
-    list_display = ['name', 'slug', 'site']
-    prepopulated_fields = {
-        'slug': ['name'],
-    }

+ 1 - 14
netbox/extras/api/serializers.py

@@ -10,8 +10,7 @@ from dcim.api.nested_serializers import (
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
 from extras.constants import *
 from extras.models import (
-    ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
-    Tag
+    ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
 )
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.models import Tenant, TenantGroup
@@ -69,18 +68,6 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
         ]
 
 
-#
-# Topology maps
-#
-
-class TopologyMapSerializer(ValidatedModelSerializer):
-    site = NestedSiteSerializer()
-
-    class Meta:
-        model = TopologyMap
-        fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
-
-
 #
 # Tags
 #

+ 0 - 3
netbox/extras/api/urls.py

@@ -26,9 +26,6 @@ router.register(r'graphs', views.GraphViewSet)
 # Export templates
 router.register(r'export-templates', views.ExportTemplateViewSet)
 
-# Topology maps
-router.register(r'topology-maps', views.TopologyMapViewSet)
-
 # Tags
 router.register(r'tags', views.TagViewSet)
 

+ 2 - 32
netbox/extras/api/views.py

@@ -2,8 +2,7 @@ from collections import OrderedDict
 
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Count
-from django.http import Http404, HttpResponse
-from django.shortcuts import get_object_or_404
+from django.http import Http404
 from rest_framework.decorators import action
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.response import Response
@@ -11,8 +10,7 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
 
 from extras import filters
 from extras.models import (
-    ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
-    Tag,
+    ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
 )
 from extras.reports import get_report, get_reports
 from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
@@ -115,34 +113,6 @@ class ExportTemplateViewSet(ModelViewSet):
     filterset_class = filters.ExportTemplateFilter
 
 
-#
-# Topology maps
-#
-
-class TopologyMapViewSet(ModelViewSet):
-    queryset = TopologyMap.objects.select_related('site')
-    serializer_class = serializers.TopologyMapSerializer
-    filterset_class = filters.TopologyMapFilter
-
-    @action(detail=True)
-    def render(self, request, pk):
-
-        tmap = get_object_or_404(TopologyMap, pk=pk)
-        img_format = 'png'
-
-        try:
-            data = tmap.render(img_format=img_format)
-        except Exception as e:
-            return HttpResponse(
-                "There was an error generating the requested graph: %s" % e
-            )
-
-        response = HttpResponse(data, content_type='image/{}'.format(img_format))
-        response['Content-Disposition'] = 'inline; filename="{}.{}"'.format(tmap.slug, img_format)
-
-        return response
-
-
 #
 # Tags
 #

+ 0 - 10
netbox/extras/constants.py

@@ -134,16 +134,6 @@ TEMPLATE_LANGUAGE_CHOICES = (
     (TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'),
 )
 
-# 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'),
-)
-
 # Change log actions
 OBJECTCHANGE_ACTION_CREATE = 1
 OBJECTCHANGE_ACTION_UPDATE = 2

+ 1 - 19
netbox/extras/filters.py

@@ -5,7 +5,7 @@ from django.db.models import Q
 from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
-from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag, TopologyMap
+from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
 
 
 class CustomFieldFilter(django_filters.Filter):
@@ -103,24 +103,6 @@ class TagFilter(django_filters.FilterSet):
         )
 
 
-class TopologyMapFilter(django_filters.FilterSet):
-    site_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='site',
-        queryset=Site.objects.all(),
-        label='Site',
-    )
-    site = django_filters.ModelMultipleChoiceFilter(
-        field_name='site__slug',
-        queryset=Site.objects.all(),
-        to_field_name='slug',
-        label='Site (slug)',
-    )
-
-    class Meta:
-        model = TopologyMap
-        fields = ['name', 'slug']
-
-
 class ConfigContextFilter(django_filters.FilterSet):
     q = django_filters.CharFilter(
         method='search',

+ 16 - 0
netbox/extras/migrations/0024_remove_topology_maps.py

@@ -0,0 +1,16 @@
+# Generated by Django 2.2 on 2019-08-09 01:26
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0023_fix_tag_sequences'),
+    ]
+
+    operations = [
+        migrations.DeleteModel(
+            name='TopologyMap',
+        ),
+    ]

+ 1 - 152
netbox/extras/models.py

@@ -7,17 +7,14 @@ from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.fields import JSONField
 from django.core.validators import ValidationError
 from django.db import models
-from django.db.models import F, Q
 from django.http import HttpResponse
 from django.template import Template, Context
 from django.urls import reverse
-import graphviz
 from jinja2 import Environment
 from taggit.models import TagBase, GenericTaggedItemBase
 
-from dcim.constants import CONNECTION_STATUS_CONNECTED
 from utilities.fields import ColorField
-from utilities.utils import deepmerge, foreground_color, model_names_to_filter_dict
+from utilities.utils import deepmerge, model_names_to_filter_dict
 from .constants import *
 from .querysets import ConfigContextQuerySet
 
@@ -496,154 +493,6 @@ class ExportTemplate(models.Model):
         return response
 
 
-#
-# Topology maps
-#
-
-class TopologyMap(models.Model):
-    name = models.CharField(
-        max_length=50,
-        unique=True
-    )
-    slug = models.SlugField(
-        unique=True
-    )
-    type = models.PositiveSmallIntegerField(
-        choices=TOPOLOGYMAP_TYPE_CHOICES,
-        default=TOPOLOGYMAP_TYPE_NETWORK
-    )
-    site = models.ForeignKey(
-        to='dcim.Site',
-        on_delete=models.CASCADE,
-        related_name='topology_maps',
-        blank=True,
-        null=True
-    )
-    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. Devices will be rendered in the '
-                  'order they are defined.'
-    )
-    description = models.CharField(
-        max_length=100,
-        blank=True
-    )
-
-    class Meta:
-        ordering = ['name']
-
-    def __str__(self):
-        return self.name
-
-    @property
-    def device_sets(self):
-        if not self.device_patterns:
-            return None
-        return [line.strip() for line in self.device_patterns.split('\n')]
-
-    def render(self, img_format='png'):
-
-        from dcim.models import Device
-
-        # Construct the graph
-        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 = 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:
-                self.graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
-
-            # Add each device to the graph
-            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))
-                subgraph.node(d.name, style='filled', fillcolor=bg_color, fontcolor=fg_color, fontname='sans')
-
-            # 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')
-
-            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 Interface
-
-        # Add all interface connections to the graph
-        connected_interfaces = Interface.objects.select_related(
-            '_connected_interface__device'
-        ).filter(
-            Q(device__in=devices) | Q(_connected_interface__device__in=devices),
-            _connected_interface__isnull=False,
-            pk__lt=F('_connected_interface')
-        )
-        for interface in connected_interfaces:
-            style = 'solid' if interface.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
-            self.graph.edge(interface.device.name, interface.connected_endpoint.device.name, style=style)
-
-        # Add all circuits to the graph
-        for termination in CircuitTermination.objects.filter(term_side='A', connected_endpoint__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):
-                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
-        for cp in ConsolePort.objects.filter(device__in=devices, connected_endpoint__device__in=devices):
-            style = 'solid' if cp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
-            self.graph.edge(cp.connected_endpoint.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
-        for pp in PowerPort.objects.filter(device__in=devices, _connected_poweroutlet__device__in=devices):
-            style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
-            self.graph.edge(pp.connected_endpoint.device.name, pp.device.name, style=style)
-
-
 #
 # Image attachments
 #

+ 1 - 2
netbox/netbox/views.py

@@ -21,7 +21,7 @@ from dcim.tables import (
     CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
     VirtualChassisTable,
 )
-from extras.models import ObjectChange, ReportResult, TopologyMap
+from extras.models import ObjectChange, ReportResult
 from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
 from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
 from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
@@ -222,7 +222,6 @@ class HomeView(View):
         return render(request, self.template_name, {
             'search_form': SearchForm(),
             'stats': stats,
-            'topology_maps': TopologyMap.objects.filter(site__isnull=True),
             'report_results': ReportResult.objects.order_by('-created')[:10],
             'changelog': ObjectChange.objects.select_related('user', 'changed_object_type')[:50]
         })

+ 0 - 19
netbox/templates/dcim/site.html

@@ -285,25 +285,6 @@
                 </div>
             {% endif %}
         </div>
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Topology Maps</strong>
-            </div>
-            {% if topology_maps %}
-                <table class="table table-hover panel-body">
-                    {% for tm in topology_maps %}
-                        <tr>
-                            <td><i class="fa fa-fw fa-map-o"></i> <a href="{% url 'extras-api:topologymap-render' pk=tm.pk %}" target="_blank">{{ tm }}</a></td>
-                            <td>{{ tm.description }}</td>
-                        </tr>
-                    {% endfor %}
-                </table>
-            {% else %}
-                <div class="panel-body text-muted">
-                    None
-                </div>
-            {% endif %}
-        </div>
 	</div>
 </div>
 {% include 'inc/modal.html' with modal_name='graphs' %}

+ 0 - 23
netbox/templates/home.html

@@ -259,29 +259,6 @@
         </div>
     </div>
     <div class="col-sm-6 col-md-4">
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Global Topology Maps</strong>
-            </div>
-            {% if topology_maps and perms.extras.view_topologymap %}
-                <table class="table table-hover panel-body">
-                    {% for tm in topology_maps %}
-                        <tr>
-                            <td><i class="fa fa-fw fa-map-o"></i> <a href="{% url 'extras-api:topologymap-render' pk=tm.pk %}" target="_blank">{{ tm }}</a></td>
-                            <td>{{ tm.description }}</td>
-                        </tr>
-                    {% endfor %}
-                </table>
-            {% elif perms.extras.view_topologymap %}
-                <div class="panel-body text-muted">
-                    None found
-                </div>
-            {% else %}
-                <div class="panel-body text-muted">
-                    <i class="fa fa-lock"></i> No permission
-                </div>
-            {% endif %}
-        </div>
         <div class="panel panel-default">
             <div class="panel-heading">
                 <strong>Reports</strong>

+ 0 - 1
requirements.txt

@@ -12,7 +12,6 @@ django-taggit-serializer==0.1.7
 django-timezone-field==3.0
 djangorestframework==3.9.4
 drf-yasg[validation]==1.16.0
-graphviz==0.10.1
 Jinja2==2.10.1
 Markdown==2.6.11
 netaddr==0.7.19