From 522a0c20e7a0c13a7a675ad67be1751613bd307d Mon Sep 17 00:00:00 2001 From: Pit Kleyersburg Date: Wed, 29 Jun 2016 11:25:36 +0200 Subject: [PATCH 01/13] Replace pydot by graphviz This is in an effort to support Python 3: pydot is not compatible with Python 3, while graphviz is. --- netbox/extras/api/views.py | 33 ++++++++++++++------------------- requirements.txt | 2 +- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 2436de4011a..81c4e917049 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,4 +1,4 @@ -import pydot +import graphviz from rest_framework import generics from rest_framework.views import APIView import tempfile @@ -49,32 +49,30 @@ class TopologyMapView(APIView): tmap = get_object_or_404(TopologyMap, slug=slug) # Construct the graph - graph = pydot.Dot(graph_type='graph', ranksep='1') + graph = graphviz.Graph() + graph.graph_attr['ranksep'] = '1' for i, device_set in enumerate(tmap.device_sets): - subgraph = pydot.Subgraph('sg{}'.format(i), rank='same') + subgraph = graphviz.Graph(name='sg{}'.format(i)) + subgraph.graph_attr['rank'] = 'same' # Add a pseudonode for each device_set to enforce hierarchical layout - subgraph.add_node(pydot.Node('set{}'.format(i), shape='none', width='0', label='')) + subgraph.node('set{}'.format(i), label='', shape='none', width='0') if i: - graph.add_edge(pydot.Edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')) + graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis') # Add each device to the graph devices = [] for query in device_set.split(','): devices += Device.objects.filter(name__regex=query) for d in devices: - node = pydot.Node(d.name) - subgraph.add_node(node) + subgraph.node(d.name) # Add an invisible connection to each successive device in a set to enforce horizontal order for j in range(0, len(devices) - 1): - edge = pydot.Edge(devices[j].name, devices[j + 1].name) - # edge.set('style', 'invis') doesn't seem to work for some reason - edge.set_style('invis') - subgraph.add_edge(edge) + subgraph.edge(devices[j].name, devices[j + 1].name, style='invis') - graph.add_subgraph(subgraph) + graph.subgraph(subgraph) # Compile list of all devices device_superset = Q() @@ -87,17 +85,14 @@ class TopologyMapView(APIView): 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) + graph.edge(c.interface_a.device.name, c.interface_b.device.name) - # Write the image to disk and return - topo_file = tempfile.NamedTemporaryFile() + # Get the image data and return try: - graph.write(topo_file.name, format='png') + topo_data = graph.pipe(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() + response = HttpResponse(topo_data, content_type='image/png') return response diff --git a/requirements.txt b/requirements.txt index 8987863a5f5..c1afc1b21fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ django-filter==0.13.0 django-rest-swagger==0.3.7 django-tables2==1.2.1 djangorestframework==3.3.3 +graphviz==0.4.10 Markdown==2.6.6 ncclient==0.4.7 netaddr==0.7.18 @@ -12,6 +13,5 @@ paramiko==2.0.0 psycopg2==2.6.1 py-gfm==0.1.3 pycrypto==2.6.1 -pydot==1.0.2 sqlparse==0.1.19 xmltodict==0.10.2 From aa000bf26d947877fc8abd67e435d68e13fdbc3a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 10:52:06 -0400 Subject: [PATCH 02/13] Fixes #110: Added status field to bulk editing form for Prefixes and VLANs --- netbox/ipam/forms.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 616f628d6ad..0c7a411cd73 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -13,6 +13,10 @@ from .models import ( ) +FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES +FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES + + # # VRFs # @@ -215,6 +219,7 @@ class PrefixBulkEditForm(forms.Form, BootstrapMixin): vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', help_text="Select the VRF to assign, or check below to remove VRF assignment") vrf_global = forms.BooleanField(required=False, label='Set VRF to global') + status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) description = forms.CharField(max_length=50, required=False) @@ -444,6 +449,7 @@ class VLANImportForm(BulkImportForm, BootstrapMixin): class VLANBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) + status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) From a1953bab8bbec4503ae9e7fb5f9f88f04141f087 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 11:04:34 -0400 Subject: [PATCH 03/13] Added a link to the GitHub issues page to the server error page --- netbox/templates/500.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/templates/500.html b/netbox/templates/500.html index a2e0fba9655..4f65eae903b 100644 --- a/netbox/templates/500.html +++ b/netbox/templates/500.html @@ -17,6 +17,8 @@

There was a problem with your request. This error has been logged and administrative staff have been notified. Please return to the home page and try again.

+

If you are responsible for this installation, please consider + filing a bug report.

From fbbdb3807ccd173835e7d32ff6e9773ee03b5f93 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 12:06:37 -0400 Subject: [PATCH 04/13] Fixes #108: Added search for Sites --- netbox/dcim/filters.py | 23 +++++++++++++++++++++++ netbox/dcim/views.py | 1 + netbox/templates/dcim/site.html | 20 ++++++++++++++++++++ netbox/templates/dcim/site_list.html | 25 ++++++++++++++++++++++++- 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 47f22168ac2..a0cb92f4d90 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -8,6 +8,29 @@ from .models import ( ) +class SiteFilter(django_filters.FilterSet): + q = django_filters.MethodFilter( + action='search', + label='Search', + ) + + class Meta: + model = Site + fields = ['q', 'name', 'facility', 'asn'] + + def search(self, queryset, value): + value = value.strip() + qs_filter = Q(name__icontains=value) |\ + Q(facility__icontains=value) |\ + Q(physical_address__icontains=value) |\ + Q(shipping_address__icontains=value) + try: + qs_filter |= Q(asn=int(value)) + except ValueError: + pass + return queryset.filter(qs_filter) + + class RackGroupFilter(django_filters.FilterSet): site_id = django_filters.ModelMultipleChoiceFilter( name='site', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 1500f096ed6..3cf157c7d5f 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -61,6 +61,7 @@ def expand_pattern(string): class SiteListView(ObjectListView): queryset = Site.objects.all() + filter = filters.SiteFilter table = tables.SiteTable template_name = 'dcim/site_list.html' diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index bddc8a5054f..a9db55e1953 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -6,6 +6,26 @@ {% block title %}{{ site }}{% endblock %} {% block content %} +
+
+ +
+
+
+
+ + + + +
+
+
+

Sites

-{% render_table table 'table.html' %} +
+
+ {% render_table table 'table.html' %} +
+
+
+
+ Search +
+
+
+
+ + + + +
+
+
+
+
+
{% endblock %} From b8b173674fdbfd62a4d5fabbe4e34cb5d1a84782 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 13:38:51 -0400 Subject: [PATCH 05/13] Fixed PEP8 error --- netbox/dcim/filters.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index a0cb92f4d90..9712899a85b 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -20,10 +20,8 @@ class SiteFilter(django_filters.FilterSet): def search(self, queryset, value): value = value.strip() - qs_filter = Q(name__icontains=value) |\ - Q(facility__icontains=value) |\ - Q(physical_address__icontains=value) |\ - Q(shipping_address__icontains=value) + qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \ + Q(shipping_address__icontains=value) try: qs_filter |= Q(asn=int(value)) except ValueError: From 48d607fb966b33876f8f7f31cf0a7d607e40dca6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 14:05:01 -0400 Subject: [PATCH 06/13] Added VERSION to settings and page footer --- netbox/netbox/settings.py | 2 ++ netbox/templates/_base.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 16bba455879..fed0c2fd727 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -11,6 +11,8 @@ except ImportError: "the documentation.") +VERSION = '1.0.6' + # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: try: diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index a67d3c440d0..1d5fa1ad537 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -237,7 +237,7 @@
-

{{ settings.HOSTNAME }}

+

{{ settings.HOSTNAME }} (v{{ settings.VERSION }})

{% now 'Y-m-d H:i:s T' %}

From 2e27389cda234ae83e0831ae14d2ef766277cfab Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 14:16:07 -0400 Subject: [PATCH 07/13] Corrected capitalization of rack face in example --- netbox/templates/dcim/device_import.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/dcim/device_import.html b/netbox/templates/dcim/device_import.html index e9eebeeaeab..db048c39d18 100644 --- a/netbox/templates/dcim/device_import.html +++ b/netbox/templates/dcim/device_import.html @@ -74,12 +74,12 @@ Face Rack face; front or rear (optional) - rear + Rear

Example

-
rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,rear
+
rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear
{% endblock %} From 76baa6fd2da046612264c6144f9092fc8e891740 Mon Sep 17 00:00:00 2001 From: bellwood Date: Wed, 29 Jun 2016 14:38:28 -0400 Subject: [PATCH 08/13] Update getting-started.md Adding instructions for Let's Encrypt SSL and enabling HTTPS in nginx --- docs/getting-started.md | 79 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/docs/getting-started.md b/docs/getting-started.md index 788c3a88f41..3b379a21823 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -354,6 +354,85 @@ At this point, you should be able to connect to the nginx HTTP service at the se Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment. +## Let's Encrypt SSL + nginx + +To add SSL support to the installation we'll start by installing the arbitrary precision calculator language. + +``` +# sudo apt-get -y bc +``` + +Next we'll clone Let’s Encrypt in to /opt + +``` +# sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt +``` + +To ensure Let's Encrypt can publicly access the directory it needs for certificate validation you'll need to edit `/etc/nginx/sites-available/netbox` and add: + +``` + location /.well-known/ { + alias /opt/netbox/netbox/.well-known/; + allow all; + } +``` + +Then restart nginix: + +``` +# sudo services nginx restart +``` + +To create the certificate use the following commands ensuring to change `netbox.example.com` to the domain name of the server: + +``` +# cd /opt/letsencrypt +# ./letsencrypt-auto certonly -a webroot --webroot-path=/opt/netbox/netbox/ -d netbox.example.com +``` + +If you wish to add support for the `www` prefix you'd use: + +``` +# cd /opt/letsencrypt +# ./letsencrypt-auto certonly -a webroot --webroot-path=/opt/netbox/netbox/ -d netbox.example.com -d www.netbox.example.com +``` + +Make sure you have DNS records setup for the hostnames you use and that they resolve back the netbox server. + +You will be prompted for your email address to receive notifications about your SSL and then asked to accept the subscriber agreement. + +If successful you'll now have four files in `/etc/letsencrypt/live/netbox.example.com` (remember, your hostname is different) + +``` +cert.pem +chain.pem +fullchain.pem +privkey.pem +``` + +Now edit your nginx configuration `/etc/nginx/sites-available/netbox` and at the top edit to the following: + +``` + #listen 80; + #listen [::]80; + listen 443; + listen [::]443; + + ssl on; + ssl_certificate /etc/letsencrypt/live/netbox.example.com/cert.pem; + ssl_certificate_key /etc/letsencrypt/live/netbox.example.com/privkey.pem; +``` + +If you are not using IPv6 then you do not need `listen [::]443;` The two commented lines are for non-SSL for both IPv4 and IPv6. + +Lastly, restart nginx: + +``` +# sudo services nginx restart +``` + +You should now have netbox running on a SSL protected connection. + # Upgrading As with the initial installation, you can upgrade NetBox by either downloading the lastest release package or by cloning the `master` branch of the git repository. Several important steps are required before running the new code. From 995447ae0b438c07d07eb339f09b220f8d820746 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 14:52:02 -0400 Subject: [PATCH 09/13] Suppressed '__all__' field name in BulkImportForm validation --- netbox/utilities/forms.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 260365eece1..9855b9273fa 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -253,6 +253,9 @@ class BulkImportForm(forms.Form): else: for field, errors in obj_form.errors.items(): for e in errors: - self.add_error('csv', "Record {} ({}): {}".format(i, field, e)) + if field == '__all__': + self.add_error('csv', "Record {}: {}".format(i, e)) + else: + self.add_error('csv', "Record {} ({}): {}".format(i, field, e)) self.cleaned_data['csv'] = obj_list From 004f5c448e44c0786eb1cc089fbcaac799fcd4e5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 14:53:24 -0400 Subject: [PATCH 10/13] Fixes #117: Improved device import validation --- netbox/dcim/forms.py | 18 +++++++++++------- netbox/dcim/models.py | 5 ++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b158a834953..e9b2f9dac42 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -427,7 +427,7 @@ class DeviceFromCSVForm(forms.ModelForm): 'invalid_choice': 'Invalid site name.', }) rack_name = forms.CharField() - face = forms.ChoiceField(choices=[('Front', 'Front'), ('Rear', 'Rear')]) + face = forms.CharField(required=False) class Meta: model = Device @@ -446,7 +446,7 @@ class DeviceFromCSVForm(forms.ModelForm): try: self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name) except DeviceType.DoesNotExist: - self.add_error('model_name', "Invalid device type ({})".format(model_name)) + self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name)) # Validate rack if site and rack_name: @@ -457,11 +457,15 @@ class DeviceFromCSVForm(forms.ModelForm): def clean_face(self): face = self.cleaned_data['face'] - if face.lower() == 'front': - return 0 - if face.lower() == 'rear': - return 1 - raise forms.ValidationError("Invalid rack face ({})".format(face)) + if face: + try: + return { + 'front': 0, + 'rear': 1, + }[face.lower()] + except KeyError: + raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face)) + return face class DeviceImportForm(BulkImportForm, BootstrapMixin): diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 013ca927b70..aeee10ee57c 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -568,7 +568,10 @@ class Device(CreatedUpdatedModel): raise ValidationError("Must specify rack face with rack position.") # Validate rack space - rack_face = self.face if not self.device_type.is_full_depth else None + try: + rack_face = self.face if not self.device_type.is_full_depth else None + except DeviceType.DoesNotExist: + raise ValidationError("Must specify device type.") exclude_list = [self.pk] if self.pk else [] try: available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face, From fc5495eb3b401096adf2aab71511feb02be5ed67 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 15:43:42 -0400 Subject: [PATCH 11/13] Introduced a script to assist with upgrading NetBox --- .gitignore | 2 +- docs/getting-started.md | 14 ++++++-------- upgrade.sh | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 9 deletions(-) create mode 100755 upgrade.sh diff --git a/.gitignore b/.gitignore index 7628f9af736..e769694eabd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ configuration.py .idea /*.sh +!upgrade.sh fabfile.py - diff --git a/docs/getting-started.md b/docs/getting-started.md index 788c3a88f41..592f658c638 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -356,19 +356,17 @@ Please keep in mind that the configurations provided here are bare minimums requ # Upgrading -As with the initial installation, you can upgrade NetBox by either downloading the lastest release package or by cloning the `master` branch of the git repository. Several important steps are required before running the new code. - -First, apply any database migrations that were included with the release. Not all releases include database migrations (in fact, most don't), so don't worry if this command returns "No migrations to apply." +As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository. Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured). ``` -# ./manage.py migrate +# ./upgrade.sh ``` -Second, collect any static file that have changed into the root static path. As with database migrations, not all releases will include changes to static files. +This script: -``` -# ./manage.py collectstatic -``` +* Installs or upgrades any new required Python packages +* Applies any database migrations that were included in the release +* Collects all static files to be served by the HTTP service Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`: diff --git a/upgrade.sh b/upgrade.sh new file mode 100755 index 00000000000..f2f1a5f2839 --- /dev/null +++ b/upgrade.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# This script will prepare NetBox to run after the code has been upgraded to +# its most recent release. +# +# Once the script completes, remember to restart the WSGI service (e.g. +# gunicorn or uWSGI). + +# Install any new Python packages +pip install -r requirements.txt --upgrade + +# Apply any database migrations +./netbox/manage.py migrate + +# Collect static files +./netbox/manage.py collectstatic --noinput From 1728d81677bbcb950f0b66ee1f1abb7d7d3c9cfc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 16:41:23 -0400 Subject: [PATCH 12/13] Added a note abotu upgrade.sh to the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ab739f9e341..cc563c284db 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net* Please see docs/getting-started.md for instructions on installing NetBox. +To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`. + # Components NetBox understands all of the physical and logical building blocks that comprise network infrastructure, and the manners in which they are all related. From 46ae4b307cb029222b27a07de2eb64cc625ff2b7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 16:44:56 -0400 Subject: [PATCH 13/13] Removed note about graphviz being optional; installing graphviz prevents confusing error messages --- docs/getting-started.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 592f658c638..b489567a4bd 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -58,14 +58,12 @@ NetBox requires following dependencies: * libxml2-dev * libxslt1-dev * libffi-dev -* graphviz* +* graphviz ``` # apt-get install python2.7 python-dev git python-pip libxml2-dev libxslt1-dev libffi-dev graphviz ``` -*graphviz is needed to render topology maps. If you have no need for this feature, graphviz is not required. - You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub. ### Option A: Download a Release