1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Moved TopologyMaps from DCIM to extras

This commit is contained in:
Jeremy Stretch
2017-03-08 16:12:14 -05:00
parent 68c099a2af
commit f43fbffdf7
8 changed files with 157 additions and 88 deletions

View File

@ -2,7 +2,6 @@ from django.conf.urls import include, url
from rest_framework import routers from rest_framework import routers
from extras.api.views import TopologyMapView
from ipam.api.views import ServiceViewSet from ipam.api.views import ServiceViewSet
from . import views from . import views
@ -55,6 +54,5 @@ urlpatterns = [
# Miscellaneous # Miscellaneous
url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'), url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'),
url(r'^topology-maps/(?P<slug>[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'),
] ]

View File

@ -208,7 +208,7 @@ class PlatformViewSet(ModelViewSet):
class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Device.objects.select_related( queryset = Device.objects.select_related(
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'rack__site', 'parent_bay', 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
).prefetch_related( ).prefetch_related(
'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
) )
@ -310,35 +310,6 @@ class InterfaceConnectionViewSet(WritableSerializerMixin, ModelViewSet):
write_serializer_class = serializers.WritableInterfaceConnectionSerializer write_serializer_class = serializers.WritableInterfaceConnectionSerializer
#
# Live queries
#
class LLDPNeighborsView(APIView):
"""
Retrieve live LLDP neighbors of a device
"""
def get(self, request, pk):
device = get_object_or_404(Device, pk=pk)
if not device.primary_ip:
raise ServiceUnavailable(detail="No IP configured for this device.")
RPC = device.get_rpc_client()
if not RPC:
raise ServiceUnavailable(detail="No RPC client available for this platform ({}).".format(device.platform))
# Connect to device and retrieve inventory info
try:
with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client:
lldp_neighbors = rpc_client.get_lldp_neighbors()
except:
raise ServiceUnavailable(detail="Error connecting to the remote device.")
return Response(lldp_neighbors)
# #
# Miscellaneous # Miscellaneous
# #

View File

@ -2,9 +2,14 @@ from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers from rest_framework import serializers
from extras.models import CustomField, CustomFieldChoice, Graph # from dcim.api.serializers import NestedSiteSerializer
from extras.models import CustomField, CustomFieldChoice, Graph, TopologyMap
#
# Custom fields
#
class CustomFieldSerializer(serializers.BaseSerializer): class CustomFieldSerializer(serializers.BaseSerializer):
""" """
Extends ModelSerializer to render any CustomFields and their values associated with an object. Extends ModelSerializer to render any CustomFields and their values associated with an object.
@ -41,6 +46,10 @@ class CustomFieldChoiceSerializer(serializers.ModelSerializer):
fields = ['id', 'value'] fields = ['id', 'value']
#
# Graphs
#
class GraphSerializer(serializers.ModelSerializer): class GraphSerializer(serializers.ModelSerializer):
embed_url = serializers.SerializerMethodField() embed_url = serializers.SerializerMethodField()
embed_link = serializers.SerializerMethodField() embed_link = serializers.SerializerMethodField()
@ -54,3 +63,22 @@ class GraphSerializer(serializers.ModelSerializer):
def get_embed_link(self, obj): def get_embed_link(self, obj):
return obj.embed_link(self.context['graphed_object']) return obj.embed_link(self.context['graphed_object'])
#
# Topology maps
#
class TopologyMapSerializer(CustomFieldModelSerializer):
# site = NestedSiteSerializer()
class Meta:
model = TopologyMap
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
class WritableTopologyMapSerializer(serializers.ModelSerializer):
class Meta:
model = TopologyMap
fields = ['name', 'slug', 'site', 'device_patterns', 'description']

16
netbox/extras/api/urls.py Normal file
View File

@ -0,0 +1,16 @@
from django.conf.urls import include, url
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(r'topology-maps', views.TopologyMapViewSet)
urlpatterns = [
url(r'', include(router.urls)),
]

View File

@ -1,6 +1,6 @@
import graphviz import graphviz
from rest_framework import generics from rest_framework import generics
from rest_framework.views import APIView from rest_framework.decorators import detail_route
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -10,9 +10,10 @@ 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 import filters
from extras.models import Graph, TopologyMap, 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 utilities.api import WritableSerializerMixin
from .serializers import GraphSerializer from . import serializers
class CustomFieldModelViewSet(ModelViewSet): class CustomFieldModelViewSet(ModelViewSet):
@ -49,7 +50,7 @@ class GraphListView(generics.ListAPIView):
""" """
Returns a list of relevant graphs Returns a list of relevant graphs
""" """
serializer_class = GraphSerializer serializer_class = serializers.GraphSerializer
def get_serializer_context(self): def get_serializer_context(self):
cls = { cls = {
@ -72,60 +73,27 @@ class GraphListView(generics.ListAPIView):
return queryset return queryset
class TopologyMapView(APIView): class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
""" queryset = TopologyMap.objects.select_related('site')
Generate a topology diagram serializer_class = serializers.TopologyMapSerializer
""" write_serializer_class = serializers.WritableTopologyMapSerializer
filter_class = filters.TopologyMapFilter
def get(self, request, slug): @detail_route()
def render(self, request, pk):
tmap = get_object_or_404(TopologyMap, slug=slug) tmap = get_object_or_404(TopologyMap, pk=pk)
format = 'png'
# Construct the graph
graph = graphviz.Graph()
graph.graph_attr['ranksep'] = '1'
for i, device_set in enumerate(tmap.device_sets):
subgraph = graphviz.Graph(name='sg{}'.format(i))
subgraph.graph_attr['rank'] = 'same'
# 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')
# Add each device to the graph
devices = []
for query in device_set.split(';'): # Split regexes on semicolons
devices += Device.objects.filter(name__regex=query)
for d in devices:
subgraph.node(d.name)
# 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')
graph.subgraph(subgraph)
# Compile list of all devices
device_superset = Q()
for device_set in tmap.device_sets:
for query in device_set.split(';'): # Split regexes on semicolons
device_superset = device_superset | Q(name__regex=query)
# Add all 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:
graph.edge(c.interface_a.device.name, c.interface_b.device.name)
# Get the image data and return
try: try:
topo_data = graph.pipe(format='png') data = tmap.render(format=format)
except: except:
return HttpResponse("There was an error generating the requested graph. Ensure that the GraphViz " return HttpResponse(
"executables have been installed correctly.") "There was an error generating the requested graph. Ensure that the GraphViz executables have been "
response = HttpResponse(topo_data, content_type='image/png') "installed correctly."
)
response = HttpResponse(data, content_type='image/{}'.format(format))
response['Content-Disposition'] = 'inline; filename="{}.{}"'.format(tmap.slug, format)
return response return response

View File

@ -2,7 +2,8 @@ import django_filters
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from .models import CF_TYPE_SELECT, CustomField from dcim.models import Site
from .models import CF_TYPE_SELECT, CustomField, TopologyMap
class CustomFieldFilter(django_filters.Filter): class CustomFieldFilter(django_filters.Filter):
@ -44,3 +45,21 @@ class CustomFieldFilterSet(django_filters.FilterSet):
custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True) custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
for cf in custom_fields: for cf in custom_fields:
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type) self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type)
class TopologyMapFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = TopologyMap
fields = ['name', 'slug']

View File

@ -1,11 +1,13 @@
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
import graphviz
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
from django.template import Template, Context from django.template import Template, Context
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
@ -66,6 +68,10 @@ ACTION_CHOICES = (
) )
#
# Custom fields
#
class CustomFieldModel(object): class CustomFieldModel(object):
def cf(self): def cf(self):
@ -211,6 +217,10 @@ class CustomFieldChoice(models.Model):
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete() CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
#
# Graphs
#
@python_2_unicode_compatible @python_2_unicode_compatible
class Graph(models.Model): class Graph(models.Model):
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES) type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
@ -236,6 +246,10 @@ class Graph(models.Model):
return template.render(Context({'obj': obj})) return template.render(Context({'obj': obj}))
#
# Export templates
#
@python_2_unicode_compatible @python_2_unicode_compatible
class ExportTemplate(models.Model): class ExportTemplate(models.Model):
content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}) content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
@ -270,6 +284,10 @@ class ExportTemplate(models.Model):
return response return response
#
# Topology maps
#
@python_2_unicode_compatible @python_2_unicode_compatible
class TopologyMap(models.Model): class TopologyMap(models.Model):
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
@ -294,6 +312,56 @@ class TopologyMap(models.Model):
return None return None
return [line.strip() for line in self.device_patterns.split('\n')] return [line.strip() for line in self.device_patterns.split('\n')]
def render(self, format='png'):
from dcim.models import Device, InterfaceConnection
# Construct the graph
graph = graphviz.Graph()
graph.graph_attr['ranksep'] = '1'
for i, device_set in enumerate(self.device_sets):
subgraph = graphviz.Graph(name='sg{}'.format(i))
subgraph.graph_attr['rank'] = 'same'
# 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')
# Add each device to the graph
devices = []
for query in device_set.split(';'): # Split regexes on semicolons
devices += Device.objects.filter(name__regex=query)
for d in devices:
subgraph.node(d.name)
# 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')
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)
# Add all 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:
graph.edge(c.interface_a.device.name, c.interface_b.device.name)
return graph.pipe(format=format)
#
# User actions
#
class UserActionManager(models.Manager): class UserActionManager(models.Manager):

View File

@ -28,6 +28,7 @@ _patterns = [
# API # API
url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')), url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')),
url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')), url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
url(r'^api/extras/', include('extras.api.urls', namespace='extras-api')),
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')), url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')), url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')), url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),