Merge pull request #18 from digitalocean/develop

Develop
This commit is contained in:
Jeremy Stretch 2016-06-24 14:04:27 -04:00 committed by GitHub
commit 063ec9a417
18 changed files with 150 additions and 104 deletions

58
docs/extras.md Normal file
View File

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

View File

@ -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',
}

View File

@ -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']

View File

@ -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'])

View File

@ -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()
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()

View File

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

View File

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

View File

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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

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

View File

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

View File

@ -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):

View File

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