mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Introduced TopologyMap
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||||
from extras.api.views import GraphListView, TopologyMapperView
|
from extras.api.views import GraphListView, TopologyMapView
|
||||||
|
|
||||||
from .views import *
|
from .views import *
|
||||||
|
|
||||||
@ -62,6 +62,6 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
|
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
|
||||||
url(r'^topology-mapper/$', TopologyMapperView.as_view(), name='topology_mapper'),
|
url(r'^topology-maps/(?P<slug>[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -15,6 +15,7 @@ from django.views.generic.edit import CreateView, UpdateView
|
|||||||
|
|
||||||
from ipam.models import Prefix, IPAddress, VLAN
|
from ipam.models import Prefix, IPAddress, VLAN
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
|
from extras.models import TopologyMap
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import ObjectListView, BulkImportView, BulkEditView, BulkDeleteView
|
from utilities.views import ObjectListView, BulkImportView, BulkEditView, BulkDeleteView
|
||||||
@ -89,10 +90,12 @@ def site(request, slug):
|
|||||||
'vlan_count': VLAN.objects.filter(site=site).count(),
|
'vlan_count': VLAN.objects.filter(site=site).count(),
|
||||||
'circuit_count': Circuit.objects.filter(site=site).count(),
|
'circuit_count': Circuit.objects.filter(site=site).count(),
|
||||||
}
|
}
|
||||||
|
topology_maps = TopologyMap.objects.filter(site=site)
|
||||||
|
|
||||||
return render(request, 'dcim/site.html', {
|
return render(request, 'dcim/site.html', {
|
||||||
'site': site,
|
'site': site,
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
|
'topology_maps': topology_maps,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Graph, ExportTemplate
|
from .models import Graph, ExportTemplate, TopologyMap
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Graph)
|
@admin.register(Graph)
|
||||||
@ -11,3 +11,11 @@ class GraphAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(ExportTemplate)
|
@admin.register(ExportTemplate)
|
||||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = ['content_type', 'name', 'mime_type', 'file_extension']
|
list_display = ['content_type', 'name', 'mime_type', 'file_extension']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(TopologyMap)
|
||||||
|
class TopologyMapAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'slug', 'site']
|
||||||
|
prepopulated_fields = {
|
||||||
|
'slug': ['name'],
|
||||||
|
}
|
||||||
|
@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404
|
|||||||
|
|
||||||
from circuits.models import Provider
|
from circuits.models import Provider
|
||||||
from dcim.models import Site, Device, Interface, InterfaceConnection
|
from dcim.models import Site, Device, Interface, InterfaceConnection
|
||||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_PROVIDER, GRAPH_TYPE_SITE
|
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_PROVIDER, GRAPH_TYPE_SITE
|
||||||
from .serializers import GraphSerializer
|
from .serializers import GraphSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -38,24 +38,23 @@ class GraphListView(generics.ListAPIView):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class TopologyMapperView(APIView):
|
class TopologyMapView(APIView):
|
||||||
"""
|
"""
|
||||||
Generate a topology diagram
|
Generate a topology diagram
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request, slug):
|
||||||
|
|
||||||
# Glean device sets to map. Each set is represented as a hierarchical tier in the diagram.
|
tmap = get_object_or_404(TopologyMap, slug=slug)
|
||||||
device_sets = request.GET.getlist('devices', [])
|
|
||||||
|
|
||||||
# Construct the graph
|
# Construct the graph
|
||||||
graph = pydot.Dot(graph_type='graph', ranksep='1')
|
graph = pydot.Dot(graph_type='graph', ranksep='1')
|
||||||
for i, device_set in enumerate(device_sets):
|
for i, device_set in enumerate(tmap.device_sets):
|
||||||
|
|
||||||
subgraph = pydot.Subgraph('sg{}'.format(i), rank='same')
|
subgraph = pydot.Subgraph('sg{}'.format(i), rank='same')
|
||||||
|
|
||||||
# Add a pseudonode for each device_set to enforce hierarchical layout
|
# Add a pseudonode for each device_set to enforce hierarchical layout
|
||||||
subgraph.add_node(pydot.Node('set{}'.format(i), shape='none'))
|
subgraph.add_node(pydot.Node('set{}'.format(i), shape='none', width='0', label=''))
|
||||||
if i:
|
if i:
|
||||||
graph.add_edge(pydot.Edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis'))
|
graph.add_edge(pydot.Edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis'))
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ class TopologyMapperView(APIView):
|
|||||||
|
|
||||||
# Compile list of all devices
|
# Compile list of all devices
|
||||||
device_superset = Q()
|
device_superset = Q()
|
||||||
for regex in device_sets:
|
for regex in tmap.device_sets:
|
||||||
device_superset = device_superset | Q(name__regex=regex)
|
device_superset = device_superset | Q(name__regex=regex)
|
||||||
|
|
||||||
# Add all connections to the graph
|
# Add all connections to the graph
|
||||||
|
31
netbox/extras/migrations/0002_topologymap.py
Normal file
31
netbox/extras/migrations/0002_topologymap.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.1 on 2016-04-08 18:53
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0005_auto_20160328_2135'),
|
||||||
|
('extras', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TopologyMap',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=50, unique=True)),
|
||||||
|
('slug', models.SlugField(unique=True)),
|
||||||
|
('device_patterns', models.TextField()),
|
||||||
|
('description', models.CharField(blank=True, max_length=100)),
|
||||||
|
('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topology_maps', to='dcim.Site')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -4,6 +4,9 @@ from django.http import HttpResponse
|
|||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
|
|
||||||
|
|
||||||
|
from dcim.models import Site
|
||||||
|
|
||||||
|
|
||||||
GRAPH_TYPE_INTERFACE = 100
|
GRAPH_TYPE_INTERFACE = 100
|
||||||
GRAPH_TYPE_PROVIDER = 200
|
GRAPH_TYPE_PROVIDER = 200
|
||||||
GRAPH_TYPE_SITE = 300
|
GRAPH_TYPE_SITE = 300
|
||||||
@ -68,3 +71,23 @@ class ExportTemplate(models.Model):
|
|||||||
filename += '.{}'.format(self.file_extension)
|
filename += '.{}'.format(self.file_extension)
|
||||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class TopologyMap(models.Model):
|
||||||
|
name = models.CharField(max_length=50, unique=True)
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
site = models.ForeignKey(Site, related_name='topology_maps', blank=True, null=True)
|
||||||
|
device_patterns = models.TextField()
|
||||||
|
description = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
def __unicode__(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')]
|
||||||
|
@ -115,6 +115,25 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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><a href="{% url 'dcim-api:topology_map' slug=tm.slug %}" 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user