mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
commit
063ec9a417
58
docs/extras.md
Normal file
58
docs/extras.md
Normal 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`.
|
@ -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',
|
||||
}
|
||||
|
||||
|
||||
|
@ -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,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'])
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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})
|
||||
|
@ -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
netbox/project-static/js/graphs.js
Normal file
26
netbox/project-static/js/graphs.js
Normal 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);
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
@ -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 %}
|
||||
|
@ -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">×</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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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">×</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
netbox/templates/inc/graphs_modal.html
Normal file
11
netbox/templates/inc/graphs_modal.html
Normal 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">×</span></button>
|
||||
<h4 class="modal-title" id="graphs_modal_title">Modal title</h4>
|
||||
</div>
|
||||
<div class="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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">×</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>
|
||||
|
@ -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">×</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>
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user