diff --git a/docs/extras.md b/docs/extras.md new file mode 100644 index 000000000..7144804db --- /dev/null +++ b/docs/extras.md @@ -0,0 +1,58 @@ +

Extras

+ +This section entails features of NetBox which are not crucial to its primary functions, but that provide additional value. + +[TOC] + +# Export Templates + +NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface. + +Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. + +Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable. Typically, you'll want to iterate through this list using a for loop. + +A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. + +## Example + +Here's an example device export template that will generate a simple Nagios configuration from a list of devices. + +``` +{% for d in queryset %}{% if d.status and d.primary_ip %}define host{ + use generic-switch + host_name {{ d.name }} + address {{ d.primary_ip.address.ip }} +} +{% endif %}{% endfor %} +``` + +The generated output will look something like this: + +``` +define host{ + use generic-switch + host_name switch1 + address 192.0.2.1 +} +define host{ + use generic-switch + host_name switch2 + address 192.0.2.2 +} +define host{ + use generic-switch + host_name switch3 + address 192.0.2.3 +} +``` + +# Graphs + +NetBox does not generate graphs itself. This feature allows you to embed contextual graphs from an external resources inside certain NetBox views. Each embedded graph must be defined with the following parameters: + +* **Type:** Interface, provider, or site. This determines where 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`. diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index c754b0296..3a8f364ef 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -145,7 +145,7 @@ class ConsolePortTemplateTable(tables.Table): empty_text = "None" show_header = False attrs = { - 'class': 'table table-hover panel-body', + 'class': 'table table-hover', } @@ -158,7 +158,7 @@ class ConsoleServerPortTemplateTable(tables.Table): empty_text = "None" show_header = False attrs = { - 'class': 'table table-hover panel-body', + 'class': 'table table-hover', } @@ -171,7 +171,7 @@ class PowerPortTemplateTable(tables.Table): empty_text = "None" show_header = False attrs = { - 'class': 'table table-hover panel-body', + 'class': 'table table-hover', } @@ -184,7 +184,7 @@ class PowerOutletTemplateTable(tables.Table): empty_text = "None" show_header = False attrs = { - 'class': 'table table-hover panel-body', + 'class': 'table table-hover', } diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 2093db31a..c5aec6732 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -19,8 +19,3 @@ class TopologyMapAdmin(admin.ModelAdmin): prepopulated_fields = { 'slug': ['name'], } - - -@admin.register(UserAction) -class UserActionAdmin(admin.ModelAdmin): - list_display = ['user', 'action', 'content_type', 'object_id', 'message'] diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index e5a17bfa9..e0f24cfe7 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -5,10 +5,14 @@ from extras.models import Graph class GraphSerializer(serializers.ModelSerializer): embed_url = serializers.SerializerMethodField() + embed_link = serializers.SerializerMethodField() class Meta: model = Graph - fields = ['name', 'embed_url', 'link'] + fields = ['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']) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 800863922..2436de401 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -84,14 +84,19 @@ class TopologyMapView(APIView): # Add all connections to the graph devices = Device.objects.filter(*(device_superset,)) - connections = InterfaceConnection.objects.filter(interface_a__device__in=devices, interface_b__device__in=devices) + connections = InterfaceConnection.objects.filter(interface_a__device__in=devices, + interface_b__device__in=devices) for c in connections: edge = pydot.Edge(c.interface_a.device.name, c.interface_b.device.name) graph.add_edge(edge) # Write the image to disk and return topo_file = tempfile.NamedTemporaryFile() - graph.write(topo_file.name, format='png') + try: + graph.write(topo_file.name, format='png') + except: + return HttpResponse("There was an error generating the requested graph. Ensure that the GraphViz " + "executables have been installed correctly.") response = HttpResponse(FileWrapper(topo_file), content_type='image/png') topo_file.close() diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 7c1ed0913..229debfd6 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -56,6 +56,12 @@ class Graph(models.Model): template = Template(self.source) return template.render(Context({'obj': obj})) + def embed_link(self, obj): + if self.link is None: + return '' + template = Template(self.link) + return template.render(Context({'obj': obj})) + class ExportTemplate(models.Model): content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}) diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 89fd6f0b7..f44fd1a24 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -258,16 +258,22 @@ ul.rack_near_face li.empty:hover a { .dark_gray:hover { background-color: #2c3e50; } /* Misc */ -.panel table>thead>tr>th { - border-bottom: 0; +.panel table { + margin-bottom: 0; } -ul.nav-tabs, ul.nav-pills { - margin-bottom: 20px; +.panel .table th { + border-bottom-width: 1px; +} +.panel table tr.even:first-child td { + border-top: 0; } .panel .list-group { max-height: 400px; overflow: auto; } +ul.nav-tabs, ul.nav-pills { + margin-bottom: 20px; +} /* Fix progress bar margin inside table cells */ td .progress { margin-bottom: 0; diff --git a/netbox/project-static/js/graphs.js b/netbox/project-static/js/graphs.js new file mode 100644 index 000000000..4405c2903 --- /dev/null +++ b/netbox/project-static/js/graphs.js @@ -0,0 +1,26 @@ +$('#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('

' + graph.name + '

'); + if (graph.embed_link) { + modal_body.append(''); + } else { + modal_body.append(''); + } + }, i*500); + }) + } + }); +}); diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index ad96f6947..333a07faf 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load static from staticfiles %} {% load helpers %} {% block title %}{{ provider }}{% endblock %} @@ -13,6 +14,10 @@
+ {% if perms.circuits.change_provider %} @@ -109,4 +114,9 @@
+{% include 'inc/graphs_modal.html' %} +{% endblock %} + +{% block javascript %} + {% endblock %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 1261cfcc2..9d9da6a18 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -327,19 +327,7 @@ {% endif %} - - - +{% include 'inc/graphs_modal.html' %} {% include 'secrets/inc/private_key_modal.html' %} {% endblock %} @@ -396,32 +384,7 @@ $(".powerport-toggle").click(function() { $(".interface-toggle").click(function() { return toggleConnection($(this), "/api/dcim/interface-connections/"); }); -$('#graphs_modal').on('show.bs.modal', function (event) { - var button = $(event.relatedTarget); - var iface = button.data('interface'); - var iface_id = button.data('id'); - var modal_title = $(this).find('.modal-title'); - var modal_body = $(this).find('.modal-body'); - modal_title.text('{{ device.name }} - ' + iface); - modal_body.empty(); - $.ajax({ - url: "/api/dcim/interfaces/" + iface_id + "/graphs/", - 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('

' + graph.name + '

'); - if (graph.link) { - modal_body.append('
'); - } else { - modal_body.append(''); - } - }, i*500); - }) - } - }); -}); + {% endblock %} diff --git a/netbox/templates/dcim/inc/_interface.html b/netbox/templates/dcim/inc/_interface.html index 9be170f0b..84af875e4 100644 --- a/netbox/templates/dcim/inc/_interface.html +++ b/netbox/templates/dcim/inc/_interface.html @@ -25,7 +25,7 @@ {% endif %} {% if iface.circuit or iface.connection %} - {% endif %} diff --git a/netbox/templates/dcim/inc/devicetype_component_table.html b/netbox/templates/dcim/inc/devicetype_component_table.html index 3c8913dc6..55bed30e9 100644 --- a/netbox/templates/dcim/inc/devicetype_component_table.html +++ b/netbox/templates/dcim/inc/devicetype_component_table.html @@ -22,8 +22,6 @@
{{ title }}
- - {% render_table table table_template|default:'table.html' %} -
+ {% render_table table 'table.html' %} {% endif %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 6ff3af6c0..8c5e8130d 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load static from staticfiles %} {% load render_table from django_tables2 %} {% load helpers %} @@ -6,7 +7,7 @@ {% block content %}
- @@ -144,47 +145,9 @@
- - +{% include 'inc/graphs_modal.html' %} {% endblock %} {% block javascript %} - + {% endblock %} diff --git a/netbox/templates/inc/graphs_modal.html b/netbox/templates/inc/graphs_modal.html new file mode 100644 index 000000000..29eaf18bf --- /dev/null +++ b/netbox/templates/inc/graphs_modal.html @@ -0,0 +1,11 @@ + diff --git a/netbox/templates/secrets/inc/private_key_modal.html b/netbox/templates/secrets/inc/private_key_modal.html index c60823973..5ab5400c3 100644 --- a/netbox/templates/secrets/inc/private_key_modal.html +++ b/netbox/templates/secrets/inc/private_key_modal.html @@ -3,7 +3,7 @@