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:
@ -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'),
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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
16
netbox/extras/api/urls.py
Normal 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)),
|
||||||
|
|
||||||
|
]
|
@ -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
|
||||||
|
@ -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']
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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')),
|
||||||
|
Reference in New Issue
Block a user