mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Closes #4349: Drop support for embedded graphs
This commit is contained in:
parent
ee34e28986
commit
ec66e1a5c0
@ -1,30 +0,0 @@
|
|||||||
# Graphs
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
Native support for embedded graphs is due to be removed in NetBox v2.10. It will likely be superseded by a plugin providing similar functionality.
|
|
||||||
|
|
||||||
NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters:
|
|
||||||
|
|
||||||
* **Type:** Site, device, provider, or interface. This determines in which view the graph will be displayed.
|
|
||||||
* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
|
|
||||||
* **Name:** The title to display above the graph.
|
|
||||||
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
|
||||||
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
|
|
||||||
|
|
||||||
Graph names and links can be rendered using Jinja2 or [Django's template language](https://docs.djangoproject.com/en/stable/ref/templates/language/).
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
|
|
||||||
```
|
|
||||||
|
|
||||||
You can define several graphs to provide multiple contexts when viewing an object. For example:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
|
||||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
|
|
||||||
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
|
||||||
```
|
|
@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
## v2.10-beta1 (FUTURE)
|
## v2.10-beta1 (FUTURE)
|
||||||
|
|
||||||
|
**NOTE:** This release completely removes support for embedded graphs.
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
|
* [#4349](https://github.com/netbox-community/netbox/issues/4349) - Dropped support for embedded graphs
|
||||||
* [#4360](https://github.com/netbox-community/netbox/issues/4360) - Remove support for the Django template language from export templates
|
* [#4360](https://github.com/netbox-community/netbox/issues/4360) - Remove support for the Django template language from export templates
|
||||||
* [#4941](https://github.com/netbox-community/netbox/issues/4941) - `commit` argument is now required argument in a custom script's `run()` method
|
* [#4941](https://github.com/netbox-community/netbox/issues/4941) - `commit` argument is now required argument in a custom script's `run()` method
|
||||||
|
|
||||||
|
@ -49,7 +49,6 @@ nav:
|
|||||||
- Custom Links: 'additional-features/custom-links.md'
|
- Custom Links: 'additional-features/custom-links.md'
|
||||||
- Custom Scripts: 'additional-features/custom-scripts.md'
|
- Custom Scripts: 'additional-features/custom-scripts.md'
|
||||||
- Export Templates: 'additional-features/export-templates.md'
|
- Export Templates: 'additional-features/export-templates.md'
|
||||||
- Graphs: 'additional-features/graphs.md'
|
|
||||||
- NAPALM: 'additional-features/napalm.md'
|
- NAPALM: 'additional-features/napalm.md'
|
||||||
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
||||||
- Reports: 'additional-features/reports.md'
|
- Reports: 'additional-features/reports.md'
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
from django.db.models import Count, Prefetch
|
from django.db.models import Count, Prefetch
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from circuits import filters
|
from circuits import filters
|
||||||
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from extras.models import Graph
|
|
||||||
from utilities.api import ModelViewSet
|
from utilities.api import ModelViewSet
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
@ -32,16 +27,6 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
|||||||
serializer_class = serializers.ProviderSerializer
|
serializer_class = serializers.ProviderSerializer
|
||||||
filterset_class = filters.ProviderFilterSet
|
filterset_class = filters.ProviderFilterSet
|
||||||
|
|
||||||
@action(detail=True)
|
|
||||||
def graphs(self, request, pk):
|
|
||||||
"""
|
|
||||||
A convenience method for rendering graphs for a particular provider.
|
|
||||||
"""
|
|
||||||
provider = get_object_or_404(self.queryset, pk=pk)
|
|
||||||
queryset = Graph.objects.restrict(request.user).filter(type__model='provider')
|
|
||||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuit Types
|
# Circuit Types
|
||||||
|
@ -22,7 +22,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||||
class Provider(ChangeLoggedModel, CustomFieldModel):
|
class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import override_settings
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.models import Graph
|
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
|
|
||||||
|
|
||||||
@ -46,27 +43,6 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
Provider.objects.bulk_create(providers)
|
Provider.objects.bulk_create(providers)
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
|
||||||
def test_get_provider_graphs(self):
|
|
||||||
"""
|
|
||||||
Test retrieval of Graphs assigned to Providers.
|
|
||||||
"""
|
|
||||||
provider = self.model.objects.first()
|
|
||||||
ct = ContentType.objects.get(app_label='circuits', model='provider')
|
|
||||||
graphs = (
|
|
||||||
Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'),
|
|
||||||
Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'),
|
|
||||||
Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'),
|
|
||||||
)
|
|
||||||
Graph.objects.bulk_create(graphs)
|
|
||||||
|
|
||||||
self.add_permissions('circuits.view_provider')
|
|
||||||
url = reverse('circuits-api:provider-graphs', kwargs={'pk': provider.pk})
|
|
||||||
response = self.client.get(url, **self.header)
|
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 3)
|
|
||||||
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?provider=provider-1&foo=1')
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count, Prefetch
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from extras.models import Graph
|
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
@ -38,7 +37,6 @@ class ProviderView(ObjectView):
|
|||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'type', 'tenant', 'terminations__site'
|
'type', 'tenant', 'terminations__site'
|
||||||
).annotate_sites()
|
).annotate_sites()
|
||||||
show_graphs = Graph.objects.filter(type__model='provider').exists()
|
|
||||||
|
|
||||||
circuits_table = tables.CircuitTable(circuits)
|
circuits_table = tables.CircuitTable(circuits)
|
||||||
circuits_table.columns.hide('provider')
|
circuits_table.columns.hide('provider')
|
||||||
@ -52,7 +50,6 @@ class ProviderView(ObjectView):
|
|||||||
return render(request, 'circuits/provider.html', {
|
return render(request, 'circuits/provider.html', {
|
||||||
'provider': provider,
|
'provider': provider,
|
||||||
'circuits_table': circuits_table,
|
'circuits_table': circuits_table,
|
||||||
'show_graphs': show_graphs,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,9 +23,7 @@ from dcim.models import (
|
|||||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||||
VirtualChassis,
|
VirtualChassis,
|
||||||
)
|
)
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from extras.models import Graph
|
|
||||||
from ipam.models import Prefix, VLAN
|
from ipam.models import Prefix, VLAN
|
||||||
from utilities.api import (
|
from utilities.api import (
|
||||||
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
|
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
|
||||||
@ -113,16 +111,6 @@ class SiteViewSet(CustomFieldModelViewSet):
|
|||||||
serializer_class = serializers.SiteSerializer
|
serializer_class = serializers.SiteSerializer
|
||||||
filterset_class = filters.SiteFilterSet
|
filterset_class = filters.SiteFilterSet
|
||||||
|
|
||||||
@action(detail=True)
|
|
||||||
def graphs(self, request, pk):
|
|
||||||
"""
|
|
||||||
A convenience method for rendering graphs for a particular site.
|
|
||||||
"""
|
|
||||||
site = get_object_or_404(self.queryset, pk=pk)
|
|
||||||
queryset = Graph.objects.restrict(request.user).filter(type__model='site')
|
|
||||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack groups
|
# Rack groups
|
||||||
@ -363,17 +351,6 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
return serializers.DeviceWithConfigContextSerializer
|
return serializers.DeviceWithConfigContextSerializer
|
||||||
|
|
||||||
@action(detail=True)
|
|
||||||
def graphs(self, request, pk):
|
|
||||||
"""
|
|
||||||
A convenience method for rendering graphs for a particular Device.
|
|
||||||
"""
|
|
||||||
device = get_object_or_404(self.queryset, pk=pk)
|
|
||||||
queryset = Graph.objects.restrict(request.user).filter(type__model='device')
|
|
||||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
|
|
||||||
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
manual_parameters=[
|
manual_parameters=[
|
||||||
Parameter(
|
Parameter(
|
||||||
@ -527,16 +504,6 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
|
|||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filterset_class = filters.InterfaceFilterSet
|
filterset_class = filters.InterfaceFilterSet
|
||||||
|
|
||||||
@action(detail=True)
|
|
||||||
def graphs(self, request, pk):
|
|
||||||
"""
|
|
||||||
A convenience method for rendering graphs for a particular interface.
|
|
||||||
"""
|
|
||||||
interface = get_object_or_404(self.queryset, pk=pk)
|
|
||||||
queryset = Graph.objects.restrict(request.user).filter(type__model='interface')
|
|
||||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class FrontPortViewSet(CableTraceMixin, ModelViewSet):
|
class FrontPortViewSet(CableTraceMixin, ModelViewSet):
|
||||||
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
|
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
|
||||||
|
@ -582,7 +582,7 @@ class BaseInterface(models.Model):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
@extras_features('graphs', 'export_templates', 'webhooks')
|
@extras_features('export_templates', 'webhooks')
|
||||||
class Interface(CableTermination, ComponentModel, BaseInterface):
|
class Interface(CableTermination, ComponentModel, BaseInterface):
|
||||||
"""
|
"""
|
||||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||||
|
@ -450,7 +450,7 @@ class Platform(ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||||
class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||||
|
@ -91,7 +91,7 @@ class Region(MPTTModel, ChangeLoggedModel):
|
|||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||||
class Site(ChangeLoggedModel, CustomFieldModel):
|
class Site(ChangeLoggedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import override_settings
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
@ -14,7 +12,6 @@ from dcim.models import (
|
|||||||
Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||||
)
|
)
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from extras.models import Graph
|
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType
|
||||||
|
|
||||||
@ -132,26 +129,6 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
|
||||||
def test_get_site_graphs(self):
|
|
||||||
"""
|
|
||||||
Test retrieval of Graphs assigned to Sites.
|
|
||||||
"""
|
|
||||||
ct = ContentType.objects.get_for_model(Site)
|
|
||||||
graphs = (
|
|
||||||
Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1'),
|
|
||||||
Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2'),
|
|
||||||
Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3'),
|
|
||||||
)
|
|
||||||
Graph.objects.bulk_create(graphs)
|
|
||||||
|
|
||||||
self.add_permissions('dcim.view_site')
|
|
||||||
url = reverse('dcim-api:site-graphs', kwargs={'pk': Site.objects.first().pk})
|
|
||||||
response = self.client.get(url, **self.header)
|
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 3)
|
|
||||||
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?site=site-1&foo=1')
|
|
||||||
|
|
||||||
|
|
||||||
class RackGroupTest(APIViewTestCases.APIViewTestCase):
|
class RackGroupTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
@ -902,26 +879,6 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
|
||||||
def test_get_device_graphs(self):
|
|
||||||
"""
|
|
||||||
Test retrieval of Graphs assigned to Devices.
|
|
||||||
"""
|
|
||||||
ct = ContentType.objects.get_for_model(Device)
|
|
||||||
graphs = (
|
|
||||||
Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?device={{ obj.name }}&foo=1'),
|
|
||||||
Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?device={{ obj.name }}&foo=2'),
|
|
||||||
Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?device={{ obj.name }}&foo=3'),
|
|
||||||
)
|
|
||||||
Graph.objects.bulk_create(graphs)
|
|
||||||
|
|
||||||
self.add_permissions('dcim.view_device')
|
|
||||||
url = reverse('dcim-api:device-graphs', kwargs={'pk': Device.objects.first().pk})
|
|
||||||
response = self.client.get(url, **self.header)
|
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 3)
|
|
||||||
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?device=Device 1&foo=1')
|
|
||||||
|
|
||||||
def test_config_context_included_by_default_in_list_view(self):
|
def test_config_context_included_by_default_in_list_view(self):
|
||||||
"""
|
"""
|
||||||
Check that config context data is included by default in the devices list.
|
Check that config context data is included by default in the devices list.
|
||||||
@ -1159,26 +1116,6 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
|
||||||
def test_get_interface_graphs(self):
|
|
||||||
"""
|
|
||||||
Test retrieval of Graphs assigned to Devices.
|
|
||||||
"""
|
|
||||||
ct = ContentType.objects.get_for_model(Interface)
|
|
||||||
graphs = (
|
|
||||||
Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'),
|
|
||||||
Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'),
|
|
||||||
Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'),
|
|
||||||
)
|
|
||||||
Graph.objects.bulk_create(graphs)
|
|
||||||
|
|
||||||
self.add_permissions('dcim.view_interface')
|
|
||||||
url = reverse('dcim-api:interface-graphs', kwargs={'pk': Interface.objects.first().pk})
|
|
||||||
response = self.client.get(url, **self.header)
|
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 3)
|
|
||||||
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?interface=Interface 1&foo=1')
|
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
|
@ -14,7 +14,6 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.models import Graph
|
|
||||||
from extras.views import ObjectConfigContextView
|
from extras.views import ObjectConfigContextView
|
||||||
from ipam.models import IPAddress, Prefix, Service, VLAN
|
from ipam.models import IPAddress, Prefix, Service, VLAN
|
||||||
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
||||||
@ -172,13 +171,11 @@ class SiteView(ObjectView):
|
|||||||
rack_groups = RackGroup.objects.restrict(request.user, 'view').filter(site=site).annotate(
|
rack_groups = RackGroup.objects.restrict(request.user, 'view').filter(site=site).annotate(
|
||||||
rack_count=Count('racks')
|
rack_count=Count('racks')
|
||||||
)
|
)
|
||||||
show_graphs = Graph.objects.filter(type__model='site').exists()
|
|
||||||
|
|
||||||
return render(request, 'dcim/site.html', {
|
return render(request, 'dcim/site.html', {
|
||||||
'site': site,
|
'site': site,
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'rack_groups': rack_groups,
|
'rack_groups': rack_groups,
|
||||||
'show_graphs': show_graphs,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -1082,8 +1079,6 @@ class DeviceView(ObjectView):
|
|||||||
'secrets': secrets,
|
'secrets': secrets,
|
||||||
'vc_members': vc_members,
|
'vc_members': vc_members,
|
||||||
'related_devices': related_devices,
|
'related_devices': related_devices,
|
||||||
'show_graphs': Graph.objects.filter(type__model='device').exists(),
|
|
||||||
'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ from django import forms
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from utilities.forms import LaxURLField
|
from utilities.forms import LaxURLField
|
||||||
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, JobResult, Webhook
|
from .models import CustomField, CustomFieldChoice, CustomLink, ExportTemplate, JobResult, Webhook
|
||||||
|
|
||||||
|
|
||||||
def order_content_types(field):
|
def order_content_types(field):
|
||||||
@ -150,45 +150,6 @@ class CustomLinkAdmin(admin.ModelAdmin):
|
|||||||
form = CustomLinkForm
|
form = CustomLinkForm
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Graphs
|
|
||||||
#
|
|
||||||
|
|
||||||
class GraphForm(forms.ModelForm):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Graph
|
|
||||||
exclude = ()
|
|
||||||
help_texts = {
|
|
||||||
'template_language': "<a href=\"https://jinja.palletsprojects.com\">Jinja2</a> is strongly recommended for "
|
|
||||||
"new graphs."
|
|
||||||
}
|
|
||||||
widgets = {
|
|
||||||
'source': forms.Textarea,
|
|
||||||
'link': forms.Textarea,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Graph)
|
|
||||||
class GraphAdmin(admin.ModelAdmin):
|
|
||||||
fieldsets = (
|
|
||||||
('Graph', {
|
|
||||||
'fields': ('type', 'name', 'weight')
|
|
||||||
}),
|
|
||||||
('Templates', {
|
|
||||||
'fields': ('template_language', 'source', 'link'),
|
|
||||||
'classes': ('monospace',)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
form = GraphForm
|
|
||||||
list_display = [
|
|
||||||
'name', 'type', 'weight', 'template_language', 'source',
|
|
||||||
]
|
|
||||||
list_filter = [
|
|
||||||
'type', 'template_language',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Export templates
|
# Export templates
|
||||||
#
|
#
|
||||||
|
@ -7,7 +7,6 @@ from utilities.api import ChoiceField, WritableNestedSerializer
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'NestedConfigContextSerializer',
|
'NestedConfigContextSerializer',
|
||||||
'NestedExportTemplateSerializer',
|
'NestedExportTemplateSerializer',
|
||||||
'NestedGraphSerializer',
|
|
||||||
'NestedImageAttachmentSerializer',
|
'NestedImageAttachmentSerializer',
|
||||||
'NestedJobResultSerializer',
|
'NestedJobResultSerializer',
|
||||||
'NestedTagSerializer',
|
'NestedTagSerializer',
|
||||||
@ -30,14 +29,6 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'name']
|
fields = ['id', 'url', 'name']
|
||||||
|
|
||||||
|
|
||||||
class NestedGraphSerializer(WritableNestedSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:graph-detail')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Graph
|
|
||||||
fields = ['id', 'url', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from dcim.api.nested_serializers import (
|
|||||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import (
|
from extras.models import (
|
||||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag,
|
ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
|
||||||
)
|
)
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||||
@ -25,43 +25,6 @@ from virtualization.models import Cluster, ClusterGroup
|
|||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Graphs
|
|
||||||
#
|
|
||||||
|
|
||||||
class GraphSerializer(ValidatedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:graph-detail')
|
|
||||||
type = ContentTypeField(
|
|
||||||
queryset=ContentType.objects.filter(FeatureQuery('graphs').get_query()),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Graph
|
|
||||||
fields = ['id', 'url', 'type', 'weight', 'name', 'template_language', 'source', 'link']
|
|
||||||
|
|
||||||
|
|
||||||
class RenderedGraphSerializer(serializers.ModelSerializer):
|
|
||||||
embed_url = serializers.SerializerMethodField(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
embed_link = serializers.SerializerMethodField(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
type = ContentTypeField(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Graph
|
|
||||||
fields = ['id', 'type', 'weight', 'name', 'embed_url', 'embed_link']
|
|
||||||
|
|
||||||
def get_embed_url(self, obj):
|
|
||||||
return obj.embed_url(self.context['graphed_object'])
|
|
||||||
|
|
||||||
def get_embed_link(self, obj):
|
|
||||||
return obj.embed_link(self.context['graphed_object'])
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Export templates
|
# Export templates
|
||||||
#
|
#
|
||||||
|
@ -8,9 +8,6 @@ router.APIRootView = views.ExtrasRootView
|
|||||||
# Custom field choices
|
# Custom field choices
|
||||||
router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
|
router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
|
||||||
|
|
||||||
# Graphs
|
|
||||||
router.register('graphs', views.GraphViewSet)
|
|
||||||
|
|
||||||
# Export templates
|
# Export templates
|
||||||
router.register('export-templates', views.ExportTemplateViewSet)
|
router.register('export-templates', views.ExportTemplateViewSet)
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from rq import Worker
|
|||||||
from extras import filters
|
from extras import filters
|
||||||
from extras.choices import JobResultStatusChoices
|
from extras.choices import JobResultStatusChoices
|
||||||
from extras.models import (
|
from extras.models import (
|
||||||
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag,
|
ConfigContext, CustomFieldChoice, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
|
||||||
)
|
)
|
||||||
from extras.reports import get_report, get_reports, run_report
|
from extras.reports import get_report, get_reports, run_report
|
||||||
from extras.scripts import get_script, get_scripts, run_script
|
from extras.scripts import get_script, get_scripts, run_script
|
||||||
@ -98,17 +98,6 @@ class CustomFieldModelViewSet(ModelViewSet):
|
|||||||
return super().get_queryset().prefetch_related('custom_field_values__field')
|
return super().get_queryset().prefetch_related('custom_field_values__field')
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Graphs
|
|
||||||
#
|
|
||||||
|
|
||||||
class GraphViewSet(ModelViewSet):
|
|
||||||
metadata_class = ContentTypeMetadata
|
|
||||||
queryset = Graph.objects.all()
|
|
||||||
serializer_class = serializers.GraphSerializer
|
|
||||||
filterset_class = filters.GraphFilterSet
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Export templates
|
# Export templates
|
||||||
#
|
#
|
||||||
|
@ -79,21 +79,6 @@ class ObjectChangeActionChoices(ChoiceSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# ExportTemplates
|
|
||||||
#
|
|
||||||
|
|
||||||
class TemplateLanguageChoices(ChoiceSet):
|
|
||||||
|
|
||||||
LANGUAGE_JINJA2 = 'jinja2'
|
|
||||||
LANGUAGE_DJANGO = 'django'
|
|
||||||
|
|
||||||
CHOICES = (
|
|
||||||
(LANGUAGE_JINJA2, 'Jinja2'),
|
|
||||||
(LANGUAGE_DJANGO, 'Django (Legacy)'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Log Levels for Reports and Scripts
|
# Log Levels for Reports and Scripts
|
||||||
#
|
#
|
||||||
|
@ -6,7 +6,6 @@ EXTRAS_FEATURES = [
|
|||||||
'custom_fields',
|
'custom_fields',
|
||||||
'custom_links',
|
'custom_links',
|
||||||
'export_templates',
|
'export_templates',
|
||||||
'graphs',
|
|
||||||
'job_results',
|
'job_results',
|
||||||
'webhooks'
|
'webhooks'
|
||||||
]
|
]
|
||||||
|
@ -7,7 +7,7 @@ from tenancy.models import Tenant, TenantGroup
|
|||||||
from utilities.filters import BaseFilterSet
|
from utilities.filters import BaseFilterSet
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, JobResult, Tag
|
from .models import ConfigContext, CustomField, ExportTemplate, ObjectChange, JobResult, Tag
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -16,7 +16,6 @@ __all__ = (
|
|||||||
'CustomFieldFilter',
|
'CustomFieldFilter',
|
||||||
'CustomFieldFilterSet',
|
'CustomFieldFilterSet',
|
||||||
'ExportTemplateFilterSet',
|
'ExportTemplateFilterSet',
|
||||||
'GraphFilterSet',
|
|
||||||
'LocalConfigContextFilterSet',
|
'LocalConfigContextFilterSet',
|
||||||
'ObjectChangeFilterSet',
|
'ObjectChangeFilterSet',
|
||||||
'TagFilterSet',
|
'TagFilterSet',
|
||||||
@ -90,13 +89,6 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
|||||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
|
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
|
||||||
|
|
||||||
|
|
||||||
class GraphFilterSet(BaseFilterSet):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Graph
|
|
||||||
fields = ['id', 'type', 'name', 'template_language']
|
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateFilterSet(BaseFilterSet):
|
class ExportTemplateFilterSet(BaseFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
16
netbox/extras/migrations/0049_remove_graph.py
Normal file
16
netbox/extras/migrations/0049_remove_graph.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 3.1 on 2020-08-21 15:47
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0048_exporttemplate_remove_template_language'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Graph',
|
||||||
|
),
|
||||||
|
]
|
@ -1,7 +1,7 @@
|
|||||||
from .change_logging import ChangeLoggedModel, ObjectChange
|
from .change_logging import ChangeLoggedModel, ObjectChange
|
||||||
from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue
|
from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue
|
||||||
from .models import (
|
from .models import (
|
||||||
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, Graph, ImageAttachment, JobResult, Report, Script,
|
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script,
|
||||||
Webhook,
|
Webhook,
|
||||||
)
|
)
|
||||||
from .tags import Tag, TaggedItem
|
from .tags import Tag, TaggedItem
|
||||||
@ -16,7 +16,6 @@ __all__ = (
|
|||||||
'CustomFieldValue',
|
'CustomFieldValue',
|
||||||
'CustomLink',
|
'CustomLink',
|
||||||
'ExportTemplate',
|
'ExportTemplate',
|
||||||
'Graph',
|
|
||||||
'ImageAttachment',
|
'ImageAttachment',
|
||||||
'JobResult',
|
'JobResult',
|
||||||
'ObjectChange',
|
'ObjectChange',
|
||||||
|
@ -203,69 +203,6 @@ class CustomLink(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Graphs
|
|
||||||
#
|
|
||||||
|
|
||||||
class Graph(models.Model):
|
|
||||||
type = models.ForeignKey(
|
|
||||||
to=ContentType,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
limit_choices_to=FeatureQuery('graphs')
|
|
||||||
)
|
|
||||||
weight = models.PositiveSmallIntegerField(
|
|
||||||
default=1000
|
|
||||||
)
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
verbose_name='Name'
|
|
||||||
)
|
|
||||||
template_language = models.CharField(
|
|
||||||
max_length=50,
|
|
||||||
choices=TemplateLanguageChoices,
|
|
||||||
default=TemplateLanguageChoices.LANGUAGE_JINJA2
|
|
||||||
)
|
|
||||||
source = models.CharField(
|
|
||||||
max_length=500,
|
|
||||||
verbose_name='Source URL'
|
|
||||||
)
|
|
||||||
link = models.URLField(
|
|
||||||
blank=True,
|
|
||||||
verbose_name='Link URL'
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ('type', 'weight', 'name', 'pk') # (type, weight, name) may be non-unique
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def embed_url(self, obj):
|
|
||||||
context = {'obj': obj}
|
|
||||||
|
|
||||||
if self.template_language == TemplateLanguageChoices.LANGUAGE_DJANGO:
|
|
||||||
template = Template(self.source)
|
|
||||||
return template.render(Context(context))
|
|
||||||
|
|
||||||
elif self.template_language == TemplateLanguageChoices.LANGUAGE_JINJA2:
|
|
||||||
return render_jinja2(self.source, context)
|
|
||||||
|
|
||||||
def embed_link(self, obj):
|
|
||||||
if self.link is None:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
context = {'obj': obj}
|
|
||||||
|
|
||||||
if self.template_language == TemplateLanguageChoices.LANGUAGE_DJANGO:
|
|
||||||
template = Template(self.link)
|
|
||||||
return template.render(Context(context))
|
|
||||||
|
|
||||||
elif self.template_language == TemplateLanguageChoices.LANGUAGE_JINJA2:
|
|
||||||
return render_jinja2(self.link, context)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Export templates
|
# Export templates
|
||||||
#
|
#
|
||||||
|
@ -10,7 +10,7 @@ from rq import Worker
|
|||||||
|
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
|
||||||
from extras.api.views import ReportViewSet, ScriptViewSet
|
from extras.api.views import ReportViewSet, ScriptViewSet
|
||||||
from extras.models import ConfigContext, ExportTemplate, Graph, ImageAttachment, Tag
|
from extras.models import ConfigContext, ExportTemplate, ImageAttachment, Tag
|
||||||
from extras.reports import Report
|
from extras.reports import Report
|
||||||
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
@ -29,39 +29,6 @@ class AppTest(APITestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class GraphTest(APIViewTestCases.APIViewTestCase):
|
|
||||||
model = Graph
|
|
||||||
brief_fields = ['id', 'name', 'url']
|
|
||||||
create_data = [
|
|
||||||
{
|
|
||||||
'type': 'dcim.site',
|
|
||||||
'name': 'Graph 4',
|
|
||||||
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'type': 'dcim.site',
|
|
||||||
'name': 'Graph 5',
|
|
||||||
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'type': 'dcim.site',
|
|
||||||
'name': 'Graph 6',
|
|
||||||
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=6',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
ct = ContentType.objects.get_for_model(Site)
|
|
||||||
|
|
||||||
graphs = (
|
|
||||||
Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?site={{ obj.name }}&foo=1'),
|
|
||||||
Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?site={{ obj.name }}&foo=2'),
|
|
||||||
Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?site={{ obj.name }}&foo=3'),
|
|
||||||
)
|
|
||||||
Graph.objects.bulk_create(graphs)
|
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
brief_fields = ['id', 'name', 'url']
|
brief_fields = ['id', 'name', 'url']
|
||||||
|
@ -2,49 +2,12 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from extras.choices import *
|
|
||||||
from extras.filters import *
|
from extras.filters import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.models import ConfigContext, ExportTemplate, Tag
|
||||||
from extras.models import ConfigContext, ExportTemplate, Graph, Tag
|
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
|
|
||||||
class GraphTestCase(TestCase):
|
|
||||||
queryset = Graph.objects.all()
|
|
||||||
filterset = GraphFilterSet
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
|
|
||||||
# Get the first three available types
|
|
||||||
content_types = ContentType.objects.filter(FeatureQuery('graphs').get_query())[:3]
|
|
||||||
|
|
||||||
graphs = (
|
|
||||||
Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'),
|
|
||||||
Graph(name='Graph 2', type=content_types[1], template_language=TemplateLanguageChoices.LANGUAGE_JINJA2, source='http://example.com/2'),
|
|
||||||
Graph(name='Graph 3', type=content_types[2], template_language=TemplateLanguageChoices.LANGUAGE_JINJA2, source='http://example.com/3'),
|
|
||||||
)
|
|
||||||
Graph.objects.bulk_create(graphs)
|
|
||||||
|
|
||||||
def test_id(self):
|
|
||||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
|
|
||||||
def test_name(self):
|
|
||||||
params = {'name': ['Graph 1', 'Graph 2']}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
|
|
||||||
def test_type(self):
|
|
||||||
content_type = ContentType.objects.filter(FeatureQuery('graphs').get_query()).first()
|
|
||||||
params = {'type': content_type.pk}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
|
||||||
|
|
||||||
def test_template_language(self):
|
|
||||||
params = {'template_language': TemplateLanguageChoices.LANGUAGE_JINJA2}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateTestCase(TestCase):
|
class ExportTemplateTestCase(TestCase):
|
||||||
queryset = ExportTemplate.objects.all()
|
queryset = ExportTemplate.objects.all()
|
||||||
filterset = ExportTemplateFilterSet
|
filterset = ExportTemplateFilterSet
|
||||||
|
@ -1,49 +1,6 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import Site
|
from extras.models import Tag
|
||||||
from extras.choices import TemplateLanguageChoices
|
|
||||||
from extras.models import Graph, Tag
|
|
||||||
|
|
||||||
|
|
||||||
class GraphTest(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
|
|
||||||
self.site = Site(name='Site 1', slug='site-1')
|
|
||||||
|
|
||||||
def test_graph_render_django(self):
|
|
||||||
|
|
||||||
# Using the pluralize filter as a sanity check (it's only available in Django)
|
|
||||||
TEMPLATE_TEXT = "{{ obj.name|lower }} thing{{ 2|pluralize }}"
|
|
||||||
RENDERED_TEXT = "site 1 things"
|
|
||||||
|
|
||||||
graph = Graph(
|
|
||||||
type=ContentType.objects.get(app_label='dcim', model='site'),
|
|
||||||
name='Graph 1',
|
|
||||||
template_language=TemplateLanguageChoices.LANGUAGE_DJANGO,
|
|
||||||
source=TEMPLATE_TEXT,
|
|
||||||
link=TEMPLATE_TEXT
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(graph.embed_url(self.site), RENDERED_TEXT)
|
|
||||||
self.assertEqual(graph.embed_link(self.site), RENDERED_TEXT)
|
|
||||||
|
|
||||||
def test_graph_render_jinja2(self):
|
|
||||||
|
|
||||||
TEMPLATE_TEXT = "{{ [obj.name, obj.slug]|join(',') }}"
|
|
||||||
RENDERED_TEXT = "Site 1,site-1"
|
|
||||||
|
|
||||||
graph = Graph(
|
|
||||||
type=ContentType.objects.get(app_label='dcim', model='site'),
|
|
||||||
name='Graph 1',
|
|
||||||
template_language=TemplateLanguageChoices.LANGUAGE_JINJA2,
|
|
||||||
source=TEMPLATE_TEXT,
|
|
||||||
link=TEMPLATE_TEXT
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(graph.embed_url(self.site), RENDERED_TEXT)
|
|
||||||
self.assertEqual(graph.embed_link(self.site), RENDERED_TEXT)
|
|
||||||
|
|
||||||
|
|
||||||
class TagTest(TestCase):
|
class TagTest(TestCase):
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
$('#graphs_modal').on('show.bs.modal', function (event) {
|
|
||||||
var button = $(event.relatedTarget);
|
|
||||||
var obj = button.data('obj');
|
|
||||||
var url = button.data('url');
|
|
||||||
var modal_title = $(this).find('.modal-title');
|
|
||||||
var modal_body = $(this).find('.modal-body');
|
|
||||||
modal_title.text(obj);
|
|
||||||
modal_body.empty();
|
|
||||||
$.ajax({
|
|
||||||
url: url,
|
|
||||||
dataType: 'json',
|
|
||||||
success: function(json) {
|
|
||||||
$.each(json, function(i, graph) {
|
|
||||||
// Build in a 500ms delay per graph to avoid hammering the server
|
|
||||||
setTimeout(function() {
|
|
||||||
modal_body.append('<h4 class="text-center">' + graph.name + '</h4>');
|
|
||||||
if (graph.embed_link) {
|
|
||||||
modal_body.append('<a href="' + graph.embed_link + '"><img src="' + graph.embed_url + '" /></a>');
|
|
||||||
} else {
|
|
||||||
modal_body.append('<img src="' + graph.embed_url + '" />');
|
|
||||||
}
|
|
||||||
}, i*500);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
@ -30,11 +30,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pull-right noprint">
|
<div class="pull-right noprint">
|
||||||
{% plugin_buttons provider %}
|
{% plugin_buttons provider %}
|
||||||
{% if show_graphs %}
|
|
||||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider-graphs' pk=provider.pk %}" title="Show graphs">
|
|
||||||
<i class="fa fa-signal" aria-hidden="true"></i> Graphs
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.circuits.add_provider %}
|
{% if perms.circuits.add_provider %}
|
||||||
{% clone_button provider %}
|
{% clone_button provider %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -138,14 +133,9 @@
|
|||||||
{% plugin_right_page provider %}
|
{% plugin_right_page provider %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% plugin_full_width_page provider %}
|
{% plugin_full_width_page provider %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
@ -38,12 +38,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pull-right noprint">
|
<div class="pull-right noprint">
|
||||||
{% plugin_buttons device %}
|
{% plugin_buttons device %}
|
||||||
{% if show_graphs %}
|
|
||||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }}" data-url="{% url 'dcim-api:device-graphs' pk=device.pk %}" title="Show graphs">
|
|
||||||
<i class="fa fa-signal" aria-hidden="true"></i>
|
|
||||||
Graphs
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.change_device %}
|
{% if perms.dcim.change_device %}
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
@ -958,7 +952,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
|
|
||||||
{% include 'secrets/inc/private_key_modal.html' %}
|
{% include 'secrets/inc/private_key_modal.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -1012,6 +1005,5 @@ $(".cable-toggle").click(function() {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
|
<script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
|
|
||||||
<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
|
<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -138,13 +138,6 @@
|
|||||||
|
|
||||||
{# Buttons #}
|
{# Buttons #}
|
||||||
<td class="text-right text-nowrap noprint">
|
<td class="text-right text-nowrap noprint">
|
||||||
{% if show_interface_graphs %}
|
|
||||||
{% if iface.connected_endpoint %}
|
|
||||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
|
|
||||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.ipam.add_ipaddress %}
|
{% if perms.ipam.add_ipaddress %}
|
||||||
<a href="{% url 'ipam:ipaddress_add' %}?interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
|
<a href="{% url 'ipam:ipaddress_add' %}?interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
|
||||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||||
|
@ -35,12 +35,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pull-right noprint">
|
<div class="pull-right noprint">
|
||||||
{% plugin_buttons site %}
|
{% plugin_buttons site %}
|
||||||
{% if show_graphs %}
|
|
||||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site-graphs' pk=site.pk %}" title="Show graphs">
|
|
||||||
<i class="fa fa-signal" aria-hidden="true"></i>
|
|
||||||
Graphs
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_site %}
|
{% if perms.dcim.add_site %}
|
||||||
{% clone_button site %}
|
{% clone_button site %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -292,14 +286,9 @@
|
|||||||
{% plugin_right_page site %}
|
{% plugin_right_page site %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% plugin_full_width_page site %}
|
{% plugin_full_width_page site %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
@ -38,11 +38,6 @@
|
|||||||
|
|
||||||
{# Buttons #}
|
{# Buttons #}
|
||||||
<td class="text-right text-nowrap noprint">
|
<td class="text-right text-nowrap noprint">
|
||||||
{% if show_interface_graphs %}
|
|
||||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ virtualmachine.name }} - {{ iface.name }}" data-url="{% url 'virtualization-api:vminterface-graphs' pk=iface.pk %}" title="Show graphs">
|
|
||||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.ipam.add_ipaddress %}
|
{% if perms.ipam.add_ipaddress %}
|
||||||
<a href="{% url 'ipam:ipaddress_add' %}?vminterface={{ iface.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
|
<a href="{% url 'ipam:ipaddress_add' %}?vminterface={{ iface.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
|
||||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from extras.models import Graph
|
|
||||||
from utilities.api import ModelViewSet
|
from utilities.api import ModelViewSet
|
||||||
from utilities.utils import get_subquery
|
from utilities.utils import get_subquery
|
||||||
from virtualization import filters
|
from virtualization import filters
|
||||||
@ -91,13 +86,3 @@ class VMInterfaceViewSet(ModelViewSet):
|
|||||||
)
|
)
|
||||||
serializer_class = serializers.VMInterfaceSerializer
|
serializer_class = serializers.VMInterfaceSerializer
|
||||||
filterset_class = filters.VMInterfaceFilterSet
|
filterset_class = filters.VMInterfaceFilterSet
|
||||||
|
|
||||||
@action(detail=True)
|
|
||||||
def graphs(self, request, pk):
|
|
||||||
"""
|
|
||||||
A convenience method for rendering graphs for a particular VM interface.
|
|
||||||
"""
|
|
||||||
vminterface = get_object_or_404(self.queryset, pk=pk)
|
|
||||||
queryset = Graph.objects.restrict(request.user).filter(type__model='vminterface')
|
|
||||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': vminterface})
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
@ -381,7 +381,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('graphs', 'export_templates', 'webhooks')
|
@extras_features('export_templates', 'webhooks')
|
||||||
class VMInterface(BaseInterface):
|
class VMInterface(BaseInterface):
|
||||||
virtual_machine = models.ForeignKey(
|
virtual_machine = models.ForeignKey(
|
||||||
to='virtualization.VirtualMachine',
|
to='virtualization.VirtualMachine',
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import override_settings
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from extras.models import Graph
|
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
@ -244,25 +241,3 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'untagged_vlan': vlans[2].pk,
|
'untagged_vlan': vlans[2].pk,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
|
||||||
def test_get_vminterface_graphs(self):
|
|
||||||
"""
|
|
||||||
Test retrieval of Graphs assigned to VM interfaces.
|
|
||||||
"""
|
|
||||||
ct = ContentType.objects.get_for_model(VMInterface)
|
|
||||||
graphs = (
|
|
||||||
Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'),
|
|
||||||
Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'),
|
|
||||||
Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'),
|
|
||||||
)
|
|
||||||
Graph.objects.bulk_create(graphs)
|
|
||||||
|
|
||||||
self.add_permissions('virtualization.view_vminterface')
|
|
||||||
url = reverse('virtualization-api:vminterface-graphs', kwargs={
|
|
||||||
'pk': VMInterface.objects.first().pk
|
|
||||||
})
|
|
||||||
response = self.client.get(url, **self.header)
|
|
||||||
|
|
||||||
self.assertEqual(len(response.data), 3)
|
|
||||||
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?interface=Interface 1&foo=1')
|
|
||||||
|
Loading…
Reference in New Issue
Block a user