Browse Source

Merge pull request #18 from digitalocean/develop

Develop
Jeremy Stretch 9 years ago
parent
commit
063ec9a417

+ 58 - 0
docs/extras.md

@@ -0,0 +1,58 @@
+<h1>Extras</h1>
+
+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`.

+ 4 - 4
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',
         }
 
 

+ 0 - 5
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']

+ 5 - 1
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'])

+ 7 - 2
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()
 

+ 6 - 0
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})

+ 10 - 4
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;

+ 26 - 0
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('<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);
+            })
+        }
+    });
+});

+ 10 - 0
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 @@
     </div>
 </div>
 <div class="pull-right">
+    <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="glyphicon glyphicon-signal" aria-hidden="true"></i>
+        Graphs
+    </button>
     {% if perms.circuits.change_provider %}
 		<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
 			<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
@@ -109,4 +114,9 @@
         </div>
     </div>
 </div>
+{% include 'inc/graphs_modal.html' %}
+{% endblock %}
+
+{% block javascript %}
+<script src="{% static 'js/graphs.js' %}"></script>
 {% endblock %}

+ 2 - 39
netbox/templates/dcim/device.html

@@ -327,19 +327,7 @@
         {% endif %}
 	</div>
 </div>
-
-<div class="modal fade" id="graphs_modal" tabindex="-1" role="dialog">
-    <div class="modal-dialog modal-lg" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-                <h4 class="modal-title" id="graphs_modal_title">Modal title</h4>
-            </div>
-            <div class="modal-body"></div>
-        </div>
-    </div>
-</div>
-
+{% 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('<h4 class="text-center">' + graph.name + '</h4>');
-                    if (graph.link) {
-                        modal_body.append('<a href="' + graph.link + '"><img src="' + graph.embed_url + '" /></a>');
-                    } else {
-                        modal_body.append('<img src="' + graph.embed_url + '" />');
-                    }
-                }, i*500);
-            })
-        }
-    });
-});
 </script>
+<script src="{% static 'js/graphs.js' %}"></script>
 <script src="{% static 'js/secrets.js' %}"></script>
 {% endblock %}

+ 1 - 1
netbox/templates/dcim/inc/_interface.html

@@ -25,7 +25,7 @@
     {% endif %}
     <td class="text-right">
         {% if iface.circuit or iface.connection %}
-            <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-interface="{{ iface.name }}" data-id="{{ iface.id }}" title="Show graphs">
+            <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 %}

+ 1 - 3
netbox/templates/dcim/inc/devicetype_component_table.html

@@ -22,8 +22,6 @@
         <div class="panel-heading">
             <strong>{{ title }}</strong>
         </div>
-        <table class="table table-hover panel-body">
-            {% render_table table table_template|default:'table.html' %}
-        </table>
+        {% render_table table 'table.html' %}
     </div>
 {% endif %}

+ 4 - 41
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 %}
 <div class="pull-right">
-    <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-site="{{ site.name }}" data-id="{{ site.id }}" title="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="glyphicon glyphicon-signal" aria-hidden="true"></i>
         Graphs
     </button>
@@ -144,47 +145,9 @@
         </div>
 	</div>
 </div>
-
-<div class="modal fade" id="graphs_modal" tabindex="-1" role="dialog">
-    <div class="modal-dialog modal-lg" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-                <h4 class="modal-title" id="graphs_modal_title">Modal title</h4>
-            </div>
-            <div class="modal-body"></div>
-        </div>
-    </div>
-</div>
+{% include 'inc/graphs_modal.html' %}
 {% endblock %}
 
 {% block javascript %}
-<script type="text/javascript">
-$('#graphs_modal').on('show.bs.modal', function (event) {
-    var button = $(event.relatedTarget);
-    var site = button.data('site');
-    var site_id = button.data('id');
-    var modal_title = $(this).find('.modal-title');
-    var modal_body = $(this).find('.modal-body');
-    modal_title.text(site);
-    modal_body.empty();
-    $.ajax({
-        url: "/api/dcim/sites/" + site_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('<h4 class="text-center">' + graph.name + '</h4>');
-                    if (graph.link) {
-                        modal_body.append('<a href="' + graph.link + '"><img src="' + graph.embed_url + '" /></a>');
-                    } else {
-                        modal_body.append('<img src="' + graph.embed_url + '" />');
-                    }
-                }, i*500);
-            })
-        }
-    });
-});
-</script>
+<script src="{% static 'js/graphs.js' %}"></script>
 {% endblock %}

+ 11 - 0
netbox/templates/inc/graphs_modal.html

@@ -0,0 +1,11 @@
+<div class="modal fade" id="graphs_modal" tabindex="-1" role="dialog">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                <h4 class="modal-title" id="graphs_modal_title">Modal title</h4>
+            </div>
+            <div class="modal-body"></div>
+        </div>
+    </div>
+</div>

+ 1 - 1
netbox/templates/secrets/inc/private_key_modal.html

@@ -3,7 +3,7 @@
         <div class="modal-content">
             <div class="modal-header">
                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-                <h4 class="modal-title" id="graphs_modal_title">
+                <h4 class="modal-title" id="privkey_modal_title">
                     <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                     Enter your private RSA key
                 </h4>

+ 1 - 1
netbox/templates/users/userkey_edit.html

@@ -46,7 +46,7 @@
         <div class="modal-content">
             <div class="modal-header">
                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-                <h4 class="modal-title" id="graphs_modal_title">
+                <h4 class="modal-title" id="new_keypair_modal_title">
                     New RSA Key Pair
                 </h4>
             </div>

+ 2 - 1
netbox/utilities/forms.py

@@ -158,7 +158,8 @@ class ExpandableNameField(forms.CharField):
     def __init__(self, *args, **kwargs):
         super(ExpandableNameField, self).__init__(*args, **kwargs)
         if not self.help_text:
-            self.help_text = 'Numeric ranges are supported for bulk creation.'
+            self.help_text = 'Numeric ranges are supported for bulk creation.<br />'\
+                             'Example: <code>ge-0/0/[0-47]</code>'
 
     def to_python(self, value):
         if re.search(EXPANSION_PATTERN, value):

+ 1 - 1
requirements.txt

@@ -12,6 +12,6 @@ paramiko==2.0.0
 psycopg2==2.6.1
 py-gfm==0.1.3
 pycrypto==2.6.1
-pydot==1.1.0
+pydot==1.0.2
 sqlparse==0.1.19
 xmltodict==0.10.2