Closes #4349: Drop support for embedded graphs

This commit is contained in:
Jeremy Stretch 2020-08-21 11:57:46 -04:00
parent ee34e28986
commit ec66e1a5c0
36 changed files with 33 additions and 595 deletions

View File

@ -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
```

View File

@ -2,8 +2,11 @@
## v2.10-beta1 (FUTURE)
**NOTE:** This release completely removes support for embedded graphs.
### 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
* [#4941](https://github.com/netbox-community/netbox/issues/4941) - `commit` argument is now required argument in a custom script's `run()` method

View File

@ -49,7 +49,6 @@ nav:
- Custom Links: 'additional-features/custom-links.md'
- Custom Scripts: 'additional-features/custom-scripts.md'
- Export Templates: 'additional-features/export-templates.md'
- Graphs: 'additional-features/graphs.md'
- NAPALM: 'additional-features/napalm.md'
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
- Reports: 'additional-features/reports.md'

View File

@ -1,14 +1,9 @@
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 circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph
from utilities.api import ModelViewSet
from . import serializers
@ -32,16 +27,6 @@ class ProviderViewSet(CustomFieldModelViewSet):
serializer_class = serializers.ProviderSerializer
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

View File

@ -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):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model

View File

@ -1,11 +1,8 @@
from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from django.urls import reverse
from circuits.choices import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Site
from extras.models import Graph
from utilities.testing import APITestCase, APIViewTestCases
@ -46,27 +43,6 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
)
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):
model = CircuitType

View File

@ -1,11 +1,10 @@
from django.conf import settings
from django.contrib import messages
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_tables2 import RequestConfig
from extras.models import Graph
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import (
@ -38,7 +37,6 @@ class ProviderView(ObjectView):
).prefetch_related(
'type', 'tenant', 'terminations__site'
).annotate_sites()
show_graphs = Graph.objects.filter(type__model='provider').exists()
circuits_table = tables.CircuitTable(circuits)
circuits_table.columns.hide('provider')
@ -52,7 +50,6 @@ class ProviderView(ObjectView):
return render(request, 'circuits/provider.html', {
'provider': provider,
'circuits_table': circuits_table,
'show_graphs': show_graphs,
})

View File

@ -23,9 +23,7 @@ from dcim.models import (
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph
from ipam.models import Prefix, VLAN
from utilities.api import (
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
@ -113,16 +111,6 @@ class SiteViewSet(CustomFieldModelViewSet):
serializer_class = serializers.SiteSerializer
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
@ -363,17 +351,6 @@ class DeviceViewSet(CustomFieldModelViewSet):
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(
manual_parameters=[
Parameter(
@ -527,16 +504,6 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
serializer_class = serializers.InterfaceSerializer
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):
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')

View File

@ -582,7 +582,7 @@ class BaseInterface(models.Model):
abstract = True
@extras_features('graphs', 'export_templates', 'webhooks')
@extras_features('export_templates', 'webhooks')
class Interface(CableTermination, ComponentModel, BaseInterface):
"""
A network interface within a Device. A physical Interface can connect to exactly one other Interface.

View File

@ -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):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,

View File

@ -91,7 +91,7 @@ class Region(MPTTModel, ChangeLoggedModel):
# Sites
#
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Site(ChangeLoggedModel, CustomFieldModel):
"""
A Site represents a geographic location within a network; typically a building or campus. The optional facility

View File

@ -1,6 +1,4 @@
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 rest_framework import status
@ -14,7 +12,6 @@ from dcim.models import (
Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
from ipam.models import VLAN
from extras.models import Graph
from utilities.testing import APITestCase, APIViewTestCases
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):
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):
"""
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):
model = FrontPort

View File

@ -14,7 +14,6 @@ from django.utils.safestring import mark_safe
from django.views.generic import View
from circuits.models import Circuit
from extras.models import Graph
from extras.views import ObjectConfigContextView
from ipam.models import IPAddress, Prefix, Service, VLAN
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_count=Count('racks')
)
show_graphs = Graph.objects.filter(type__model='site').exists()
return render(request, 'dcim/site.html', {
'site': site,
'stats': stats,
'rack_groups': rack_groups,
'show_graphs': show_graphs,
})
@ -1082,8 +1079,6 @@ class DeviceView(ObjectView):
'secrets': secrets,
'vc_members': vc_members,
'related_devices': related_devices,
'show_graphs': Graph.objects.filter(type__model='device').exists(),
'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(),
})

View File

@ -2,7 +2,7 @@ from django import forms
from django.contrib import admin
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):
@ -150,45 +150,6 @@ class CustomLinkAdmin(admin.ModelAdmin):
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
#

View File

@ -7,7 +7,6 @@ from utilities.api import ChoiceField, WritableNestedSerializer
__all__ = [
'NestedConfigContextSerializer',
'NestedExportTemplateSerializer',
'NestedGraphSerializer',
'NestedImageAttachmentSerializer',
'NestedJobResultSerializer',
'NestedTagSerializer',
@ -30,14 +29,6 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
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):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')

View File

@ -10,7 +10,7 @@ from dcim.api.nested_serializers import (
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
from extras.choices import *
from extras.models import (
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag,
ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
)
from extras.utils import FeatureQuery
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
@ -25,43 +25,6 @@ from virtualization.models import Cluster, ClusterGroup
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
#

View File

@ -8,9 +8,6 @@ router.APIRootView = views.ExtrasRootView
# Custom field choices
router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
# Graphs
router.register('graphs', views.GraphViewSet)
# Export templates
router.register('export-templates', views.ExportTemplateViewSet)

View File

@ -15,7 +15,7 @@ from rq import Worker
from extras import filters
from extras.choices import JobResultStatusChoices
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.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')
#
# Graphs
#
class GraphViewSet(ModelViewSet):
metadata_class = ContentTypeMetadata
queryset = Graph.objects.all()
serializer_class = serializers.GraphSerializer
filterset_class = filters.GraphFilterSet
#
# Export templates
#

View File

@ -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
#

View File

@ -6,7 +6,6 @@ EXTRAS_FEATURES = [
'custom_fields',
'custom_links',
'export_templates',
'graphs',
'job_results',
'webhooks'
]

View File

@ -7,7 +7,7 @@ from tenancy.models import Tenant, TenantGroup
from utilities.filters import BaseFilterSet
from virtualization.models import Cluster, ClusterGroup
from .choices import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, JobResult, Tag
from .models import ConfigContext, CustomField, ExportTemplate, ObjectChange, JobResult, Tag
__all__ = (
@ -16,7 +16,6 @@ __all__ = (
'CustomFieldFilter',
'CustomFieldFilterSet',
'ExportTemplateFilterSet',
'GraphFilterSet',
'LocalConfigContextFilterSet',
'ObjectChangeFilterSet',
'TagFilterSet',
@ -90,13 +89,6 @@ class CustomFieldFilterSet(django_filters.FilterSet):
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 Meta:

View 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',
),
]

View File

@ -1,7 +1,7 @@
from .change_logging import ChangeLoggedModel, ObjectChange
from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue
from .models import (
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, Graph, ImageAttachment, JobResult, Report, Script,
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script,
Webhook,
)
from .tags import Tag, TaggedItem
@ -16,7 +16,6 @@ __all__ = (
'CustomFieldValue',
'CustomLink',
'ExportTemplate',
'Graph',
'ImageAttachment',
'JobResult',
'ObjectChange',

View File

@ -203,69 +203,6 @@ class CustomLink(models.Model):
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
#

View File

@ -10,7 +10,7 @@ from rq import Worker
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
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.scripts import BooleanVar, IntegerVar, Script, StringVar
from utilities.testing import APITestCase, APIViewTestCases
@ -29,39 +29,6 @@ class AppTest(APITestCase):
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):
model = ExportTemplate
brief_fields = ['id', 'name', 'url']

View File

@ -2,49 +2,12 @@ from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from dcim.models import DeviceRole, Platform, Region, Site
from extras.choices import *
from extras.filters import *
from extras.utils import FeatureQuery
from extras.models import ConfigContext, ExportTemplate, Graph, Tag
from extras.models import ConfigContext, ExportTemplate, Tag
from tenancy.models import Tenant, TenantGroup
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):
queryset = ExportTemplate.objects.all()
filterset = ExportTemplateFilterSet

View File

@ -1,49 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from dcim.models import Site
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)
from extras.models import Tag
class TagTest(TestCase):

View File

@ -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);
})
}
});
});

View File

@ -30,11 +30,6 @@
</div>
<div class="pull-right noprint">
{% 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 %}
{% clone_button provider %}
{% endif %}
@ -138,14 +133,9 @@
{% plugin_right_page provider %}
</div>
</div>
{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
<div class="row">
<div class="col-md-12">
{% plugin_full_width_page provider %}
</div>
</div>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
{% endblock %}

View File

@ -38,12 +38,6 @@
</div>
<div class="pull-right noprint">
{% 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 %}
<div class="btn-group">
<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>
{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
{% include 'secrets/inc/private_key_modal.html' %}
{% endblock %}
@ -1012,6 +1005,5 @@ $(".cable-toggle").click(function() {
});
</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>
{% endblock %}

View File

@ -138,13 +138,6 @@
{# Buttons #}
<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 %}
<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>

View File

@ -35,12 +35,6 @@
</div>
<div class="pull-right noprint">
{% 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 %}
{% clone_button site %}
{% endif %}
@ -292,14 +286,9 @@
{% plugin_right_page site %}
</div>
</div>
{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
<div class="row">
<div class="col-md-12">
{% plugin_full_width_page site %}
</div>
</div>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
{% endblock %}

View File

@ -38,11 +38,6 @@
{# Buttons #}
<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 %}
<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>

View File

@ -1,13 +1,8 @@
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 dcim.models import Device
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph
from utilities.api import ModelViewSet
from utilities.utils import get_subquery
from virtualization import filters
@ -91,13 +86,3 @@ class VMInterfaceViewSet(ModelViewSet):
)
serializer_class = serializers.VMInterfaceSerializer
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)

View File

@ -381,7 +381,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
# Interfaces
#
@extras_features('graphs', 'export_templates', 'webhooks')
@extras_features('export_templates', 'webhooks')
class VMInterface(BaseInterface):
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',

View File

@ -1,10 +1,7 @@
from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from django.urls import reverse
from rest_framework import status
from dcim.choices import InterfaceModeChoices
from extras.models import Graph
from ipam.models import VLAN
from utilities.testing import APITestCase, APIViewTestCases
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -244,25 +241,3 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
'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')