From 1ea8f04c23eae58eeea3a060230f267b69efe001 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jun 2016 15:23:06 -0400 Subject: [PATCH 01/64] Added a note about the IRC chanel to the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ba736fee8..becf60963 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ NetBox is an IP address management (IPAM) and data center infrastructure managem NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox). +Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net**! + ![Screenshot of main page](docs/screenshot1.png "Main page") ![Screenshot of rack elevation](docs/screenshot2.png "Rack elevation") From ab880e105392ca3640eda4bc20368e0f57face2e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jun 2016 15:51:47 -0400 Subject: [PATCH 02/64] Fixed IPAddress 'parent prefixes' display; added warning for duplicate IPs --- netbox/ipam/views.py | 16 ++++++++++---- netbox/templates/ipam/ipaddress.html | 33 +++++++--------------------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 75c582b99..b230783fc 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -395,16 +395,24 @@ def ipaddress(request, pk): ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk) + # Parent prefixes table parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)) - related_ips = IPAddress.objects.select_related('interface__device').exclude(pk=ipaddress.pk)\ - .filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)) + parent_prefixes_table = tables.PrefixBriefTable(parent_prefixes) + # Duplicate IPs table + duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\ + .exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside') + duplicate_ips_table = tables.IPAddressBriefTable(duplicate_ips) + + # Related IP table + related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\ + .filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)) related_ips_table = tables.IPAddressBriefTable(related_ips) - RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(related_ips_table) return render(request, 'ipam/ipaddress.html', { 'ipaddress': ipaddress, - 'parent_prefixes': parent_prefixes, + 'parent_prefixes_table': parent_prefixes_table, + 'duplicate_ips_table': duplicate_ips_table, 'related_ips_table': related_ips_table, }) diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 4c2d11f10..6834ded9e 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -119,31 +119,14 @@
-
-
- Parent Prefixes -
- {% if parent_prefixes %} - - {% for p in parent_prefixes %} - - - - - - - {% endfor %} -
- {{ p }} - - {% if p.site %} - {{ p.site }} - {% endif %} - {{ p.status }}{{ p.role }}
- {% else %} -
None
- {% endif %} -
+ {% with heading='Parent Prefixes' %} + {% render_table parent_prefixes_table 'panel_table.html' %} + {% endwith %} + {% if duplicate_ips_table.rows %} + {% with heading='Duplicate IP Addresses' panel_class='danger' %} + {% render_table duplicate_ips_table 'panel_table.html' %} + {% endwith %} + {% endif %} {% with heading='Related IP Addresses' %} {% render_table related_ips_table 'panel_table.html' %} {% endwith %} From d5d4eb9fd5a3acac136f74ea6d99082d04982acf Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Mon, 27 Jun 2016 16:48:54 -0400 Subject: [PATCH 03/64] Add Travis CI build --- .gitignore | 2 +- .travis.yml | 7 +++++++ README.md | 2 ++ scripts/cibuild.sh | 28 ++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .travis.yml create mode 100755 scripts/cibuild.sh diff --git a/.gitignore b/.gitignore index e8ff56275..83343ee0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ *.pyc configuration.py .idea -*.sh +./*.sh fabfile.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..a7f9cda45 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: + - "2.7" +install: + - pip install -r requirements.txt +script: + - ./scripts/cibuild.sh diff --git a/README.md b/README.md index becf60963..e9bbf689b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# NetBox [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=master)](https://travis-ci.org/digitalocean/netbox) + NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox). diff --git a/scripts/cibuild.sh b/scripts/cibuild.sh new file mode 100755 index 000000000..91a847c37 --- /dev/null +++ b/scripts/cibuild.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Exit code starts at 0 but is modified if any checks fail +EXIT=0 + +# Output a line prefixed with a timestamp +info() +{ + echo "$(date +'%F %T') |" +} + +# Track number of seconds required to run script +START=$(date +%s) +echo "$(info) starting build checks." + +# Syntax check all python source files +SYNTAX=$(find . -name "*.py" -type f -exec python -m py_compile {} \; 2>&1) +if [[ ! -z $SYNTAX ]]; then + echo -e "$SYNTAX" + echo -e "\n$(info) detected one or more syntax errors, failing build." + EXIT=1 +fi + +# Show build duration +END=$(date +%s) +echo "$(info) exiting with code $EXIT after $(($END - $START)) seconds." + +exit $EXIT From e334c64a7cf0265a501090e2c6609771f97d620f Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Mon, 27 Jun 2016 19:49:57 -0400 Subject: [PATCH 04/64] Add Submitting Pull Requests section to CONTRIBUTING --- CONTRIBUTING.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5e9bd38e..3c36885ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,3 +48,9 @@ Even if it's not quite right for NetBox, we may be able to point you to a tool b * A use case for the feature; who would use it and what value it would add to NetBox * A rough description of any changes necessary to the database schema (if applicable) * Any third-party libraries or other resources which would be involved + +# Submitting Pull Requests + +When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`. +In NetBox, the `develop` branch is used for ongoing development, while `master` is used for tagging new +stable releases. From f0fb60734a2d9383af25a6f44aaf8b029e49977f Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Mon, 27 Jun 2016 19:55:17 -0400 Subject: [PATCH 05/64] Add Travis build badges for both master and develop against python 2.7 --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e9bbf689b..ab739f9e3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# NetBox [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=master)](https://travis-ci.org/digitalocean/netbox) +# NetBox NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. @@ -6,6 +6,15 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net**! +### Build Status + +| | python 2.7 | +|-------------|------------| +| **master** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=master)](https://travis-ci.org/digitalocean/netbox) | +| **develop** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=develop)](https://travis-ci.org/digitalocean/netbox) | + +## Screenshots + ![Screenshot of main page](docs/screenshot1.png "Main page") ![Screenshot of rack elevation](docs/screenshot2.png "Rack elevation") From a4cbfd7d5b2289fd8f874391afa8c5b031d06b3b Mon Sep 17 00:00:00 2001 From: Alex Conrey Date: Mon, 27 Jun 2016 19:51:46 -0500 Subject: [PATCH 06/64] added apache config information to getting-started.md --- docs/getting-started.md | 47 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 8f2688aef..7395fdace 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -206,20 +206,26 @@ Now if we navigate to the name or IP of the server (as defined in `ALLOWED_HOSTS If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected. -# nginx and gunicorn +# Web Server and gunicorn ## Installation -We'll set up a simple HTTP front end using [nginx](https://www.nginx.com/resources/wiki/) and [gunicorn](http://gunicorn.org/) for the purposes of this guide. (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) for service persistence. +We'll set up a simple HTTP front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we have 2 configurations ready to go - we provide instructions for both [nginx](https://www.nginx.com/resources/wiki/)and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) for service persistence. ``` -# apt-get install nginx gunicorn supervisor +# apt-get install gunicorn supervisor ``` ## nginx Configuration The following will serve as a minimal nginx configuration. Be sure to modify your server name and installation path appropriately. +``` +# apt-get install nginx +``` + +Once nginx is installed, proceed with the following configuration: + ``` server { listen 80; @@ -256,6 +262,40 @@ Restart the nginx service to use the new configuration. # service nginx restart * Restarting nginx nginx ``` +## Apache Configuration + +If you're feeling adventurous, or you already have Apache installed and can't run a dual-stack on your server - an Apache configuration has been created: + +``` + + ProxyPreserveHost On + + ServerName netbox.totallycool.tld + + Alias /static/ /opt/netbox/static/static + + + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Order allow,deny + Allow from all + #Require all granted [UNCOMMENT THIS IF RUNNING APACHE 2.4] + + + + ProxyPass ! + + + ProxyPass / http://127.0.0.1:8001; + ProxyPassReverse / http://127.0.0.1:8001; + +``` + +Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf`, add in the newly saved configuration and reload Apache: + +``` +# a2ensite netbox; service apache2 restart +``` ## gunicorn Configuration @@ -289,3 +329,4 @@ Finally, restart the supervisor service to detect and run the gunicorn service: At this point, you should be able to connect to the nginx HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running. Please keep in mind that the configurations provided here are a bare minimum to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment. + From 4dd31497e52013ec45d3b766d9b8345e2dd238bb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jun 2016 22:27:40 -0400 Subject: [PATCH 07/64] Fixes #26: Corrected rack validation to work when there are no devices within the rack --- netbox/dcim/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 5ff233c9c..0c587dd3d 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -188,10 +188,11 @@ class Rack(CreatedUpdatedModel): # Validate that Rack is tall enough to house the installed Devices if self.pk: top_device = Device.objects.filter(rack=self).order_by('-position').first() - min_height = top_device.position + top_device.device_type.u_height - 1 - if self.u_height < min_height: - raise ValidationError("Rack must be at least {}U tall with currently installed devices." - .format(min_height)) + if top_device: + min_height = top_device.position + top_device.device_type.u_height - 1 + if self.u_height < min_height: + raise ValidationError("Rack must be at least {}U tall with currently installed devices." + .format(min_height)) def to_csv(self): return ','.join([ From df01947c9ef9bf0c592e01722cb6a11d374ffd57 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jun 2016 22:35:07 -0400 Subject: [PATCH 08/64] Corrected claim about RIRs being pre-populated --- docs/ipam.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ipam.md b/docs/ipam.md index 295b32ccc..d37a16319 100644 --- a/docs/ipam.md +++ b/docs/ipam.md @@ -36,7 +36,7 @@ Any prefixes you create in NetBox (discussed below) will be automatically organi Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. -Each aggregate must be assigned to one RIR. NetBox by default will be populated with the RIRs listed above, however you are free to remove these and/or create your own if you choose. +Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). --- From 9aa0972a8c79aa531a4e3ceddb2e5100a61c23e6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jun 2016 22:48:24 -0400 Subject: [PATCH 09/64] Corrected regex to ignore shell files in root dir --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 83343ee0a..7628f9af7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ *.pyc configuration.py .idea -./*.sh +/*.sh fabfile.py From 7918f85cdd0472a37cf8a16d545cb68201d112fb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jun 2016 23:08:30 -0400 Subject: [PATCH 10/64] Corrected rack height validation to exclude 0U devices --- netbox/dcim/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 0c587dd3d..b6ec28be2 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -187,7 +187,7 @@ class Rack(CreatedUpdatedModel): # Validate that Rack is tall enough to house the installed Devices if self.pk: - top_device = Device.objects.filter(rack=self).order_by('-position').first() + top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first() if top_device: min_height = top_device.position + top_device.device_type.u_height - 1 if self.u_height < min_height: From 4e5f537cc5f8f1d12c28c14de29d38296c4bef74 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jun 2016 23:18:26 -0400 Subject: [PATCH 11/64] When editing an object, cancel_url should point to its normal view; when adding, it should point to the object list --- netbox/utilities/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index e1611a7ac..671390060 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -120,7 +120,7 @@ class ObjectEditView(View): 'obj': obj, 'obj_type': self.model._meta.verbose_name, 'form': form, - 'cancel_url': reverse(self.cancel_url) if self.cancel_url else obj.get_absolute_url(), + 'cancel_url': obj.get_absolute_url() if obj else reverse(self.cancel_url), }) def post(self, request, *args, **kwargs): @@ -157,7 +157,7 @@ class ObjectEditView(View): 'obj': obj, 'obj_type': self.model._meta.verbose_name, 'form': form, - 'cancel_url': reverse(self.cancel_url) if self.cancel_url else obj.get_absolute_url(), + 'cancel_url': obj.get_absolute_url() if obj else reverse(self.cancel_url), }) From 2080abc6c3d84457ef7242ebd364460834a2cc2b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jun 2016 23:56:39 -0400 Subject: [PATCH 12/64] Corrected SiteTest to account for earlier Graph model change --- netbox/dcim/tests/test_apis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index ffb74cea2..f36fc4e84 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -47,7 +47,7 @@ class SiteTest(APITestCase): graph_fields = [ 'name', 'embed_url', - 'link', + 'embed_link', ] def test_get_list(self, endpoint='/api/dcim/sites/'): From c5d498ac148a0c41b1864804e37a9023e5d2fcfc Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Mon, 27 Jun 2016 16:58:00 -0400 Subject: [PATCH 13/64] Run tests in CI --- scripts/cibuild.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/cibuild.sh b/scripts/cibuild.sh index 91a847c37..b3f50152e 100755 --- a/scripts/cibuild.sh +++ b/scripts/cibuild.sh @@ -21,6 +21,20 @@ if [[ ! -z $SYNTAX ]]; then EXIT=1 fi +# Prepare configuration file for use in CI +CONFIG="netbox/netbox/configuration.py" +cp netbox/netbox/configuration.example.py $CONFIG +sed -i -e "s/ALLOWED_HOSTS = \[\]/ALLOWED_HOSTS = \['*'\]/g" $CONFIG +sed -i -e "s/SECRET_KEY = ''/SECRET_KEY = 'netboxci'/g" $CONFIG + +# Run NetBox tests +./netbox/manage.py test netbox/ +RC=$? +if [[ $RC != 0 ]]; then + echo -e "\n$(info) one or more tests failed, failing build." + EXIT=$RC +fi + # Show build duration END=$(date +%s) echo "$(info) exiting with code $EXIT after $(($END - $START)) seconds." From 5181c97281e964da0b89176a50d476819039d658 Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Tue, 28 Jun 2016 00:25:12 -0400 Subject: [PATCH 14/64] Fix PEP 8 error in DCIM tests --- netbox/dcim/tests/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index ca841ea8f..2f3d8def6 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -64,7 +64,7 @@ class RackTestCase(TestCase): rack=rack1, position=10, face=RACK_FACE_REAR, - ) + ) device1.save() # Validate rack height From b392aa4a4a7a968f1de5956f5c98727856fb7637 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 09:39:55 -0400 Subject: [PATCH 15/64] Fixes #45: Strip plus signs during slugification --- netbox/project-static/js/forms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 793a7c8e2..4ba61f13a 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -7,9 +7,9 @@ $(document).ready(function() { // Slugify function slugify(s, num_chars) { - s = s.replace(/[^-\.\+\w\s]/g, ''); // Remove unneeded chars + s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars s = s.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing spaces - s = s.replace(/[-\s]+/g, '-'); // Convert spaces to hyphens + s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens s = s.toLowerCase(); // Convert to lowercase return s.substring(0, num_chars); // Trim to first num_chars chars } From 4dac43c1c932d44915c64301f6294b818a21e157 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 09:50:00 -0400 Subject: [PATCH 16/64] Fixes #48: Set .container to auto with a max width --- netbox/project-static/css/base.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index f44fd1a24..04bab2c63 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -9,7 +9,8 @@ body { padding-top: 70px; } .container { - width: 1340px; + width: auto; + max-width: 1340px; } .wrapper { min-height: 100%; From 6848a3dc810758d267a5f4a1bc4e6c08340c466e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 10:04:03 -0400 Subject: [PATCH 17/64] Fixes #67: Improved Aggregate validation; extended aggregate documentation --- docs/ipam.md | 2 ++ netbox/ipam/models.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/docs/ipam.md b/docs/ipam.md index d37a16319..53c5858f2 100644 --- a/docs/ipam.md +++ b/docs/ipam.md @@ -32,6 +32,8 @@ Additionally, you might define an aggregate for each large swath of public IPv4 Any prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation. +Aggregates cannot overlap with one another; they can only exist in parallel. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix. + ### RIRs Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 91d04a09a..4dbfa801a 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -121,6 +121,12 @@ class Aggregate(CreatedUpdatedModel): raise ValidationError("{} is already covered by an existing aggregate ({})" .format(self.prefix, covering_aggregates[0])) + # Ensure that the aggregate being added does not cover an existing aggregate + covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix)) + if covered_aggregates: + raise ValidationError("{} is overlaps with an existing aggregate ({})" + .format(self.prefix, covered_aggregates[0])) + def save(self, *args, **kwargs): if self.prefix: # Infer address family from IPNetwork object From f1857dd189442211c4a9a377ed1ee853a4424309 Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Tue, 28 Jun 2016 00:20:02 -0400 Subject: [PATCH 18/64] Add CI check for PEP 8 compliance --- .travis.yml | 1 + scripts/cibuild.sh | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.travis.yml b/.travis.yml index a7f9cda45..01fb25d8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,6 @@ python: - "2.7" install: - pip install -r requirements.txt + - pip install pep8 script: - ./scripts/cibuild.sh diff --git a/scripts/cibuild.sh b/scripts/cibuild.sh index b3f50152e..4f4fe1ca3 100755 --- a/scripts/cibuild.sh +++ b/scripts/cibuild.sh @@ -21,6 +21,16 @@ if [[ ! -z $SYNTAX ]]; then EXIT=1 fi +# Check all python source files for PEP 8 compliance, but explicitly +# ignore: +# - E501: line greater than 80 characters in length +pep8 --ignore=E501 netbox/ +RC=$? +if [[ $RC != 0 ]]; then + echo -e "\n$(info) one or more PEP 8 errors detected, failing build." + EXIT=$RC +fi + # Prepare configuration file for use in CI CONFIG="netbox/netbox/configuration.py" cp netbox/netbox/configuration.example.py $CONFIG From 9acd0e99f99c3a5ec5214e60b491fa517f70aadf Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Tue, 28 Jun 2016 10:55:38 -0400 Subject: [PATCH 19/64] Add sanity check checklist for submitting pull requests --- CONTRIBUTING.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c36885ea..f437d2516 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,8 +49,14 @@ Even if it's not quite right for NetBox, we may be able to point you to a tool b * A rough description of any changes necessary to the database schema (if applicable) * Any third-party libraries or other resources which would be involved -# Submitting Pull Requests +## Submitting Pull Requests -When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`. +* When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`. In NetBox, the `develop` branch is used for ongoing development, while `master` is used for tagging new stable releases. + +* All code submissions should meet the following criteria (CI will enforce these checks): + + * Python syntax is valid + * All tests pass when run with `./manage.py test netbox/` + * PEP 8 compliance is enforced, with the exception that lines may be greater than 80 characters in length From 98febf3979c3c128ad36fb05a9509f9b1cd012c0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 11:11:53 -0400 Subject: [PATCH 20/64] Fixes #72: Check for re-used interfaces when importing interface connections --- netbox/dcim/forms.py | 9 +++++++++ netbox/templates/dcim/interface_connections_import.html | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 3b7c09ee6..81b75791e 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1036,20 +1036,29 @@ class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin): return connection_list = [] + occupied_interfaces = [] for i, record in enumerate(records, start=1): form = self.fields['csv'].csv_form(data=record) if form.is_valid(): interface_a = Interface.objects.get(device=form.cleaned_data['device_a'], name=form.cleaned_data['interface_a']) + if interface_a in occupied_interfaces: + raise forms.ValidationError("{} {} found in multiple connections" + .format(interface_a.device.name, interface_a.name)) interface_b = Interface.objects.get(device=form.cleaned_data['device_b'], name=form.cleaned_data['interface_b']) + if interface_b in occupied_interfaces: + raise forms.ValidationError("{} {} found in multiple connections" + .format(interface_b.device.name, interface_b.name)) connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b) if form.cleaned_data['status'] == 'planned': connection.connection_status = CONNECTION_STATUS_PLANNED else: connection.connection_status = CONNECTION_STATUS_CONNECTED connection_list.append(connection) + occupied_interfaces.append(interface_a) + occupied_interfaces.append(interface_b) else: for field, errors in form.errors.items(): for e in errors: diff --git a/netbox/templates/dcim/interface_connections_import.html b/netbox/templates/dcim/interface_connections_import.html index 79fce2eb2..6329e0680 100644 --- a/netbox/templates/dcim/interface_connections_import.html +++ b/netbox/templates/dcim/interface_connections_import.html @@ -8,6 +8,14 @@

Interface Connections Import

+ {% if form.non_field_errors %} +
+
Errors
+
+ {{ form.non_field_errors }} +
+
+ {% endif %}
{% csrf_token %} {% render_form form %} From 374702927b2aaace53a37e30846fc959a384c34e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 11:38:09 -0400 Subject: [PATCH 21/64] Fixes #80: Correct rack face (lowercase) to be consistent with export behavior (uppercase) --- netbox/dcim/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 81b75791e..776306a07 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -424,7 +424,7 @@ class DeviceFromCSVForm(forms.ModelForm): 'invalid_choice': 'Invalid site name.', }) rack_name = forms.CharField() - face = forms.ChoiceField(choices=[('front', 'Front'), ('rear', 'Rear')]) + face = forms.ChoiceField(choices=[('Front', 'Front'), ('Rear', 'Rear')]) class Meta: model = Device From b37503ed8fec7ee3d88e6cca7924fd10c08e5705 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 11:50:25 -0400 Subject: [PATCH 22/64] Corrected typos in the Apache config; cleaned up grammar --- docs/getting-started.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 7395fdace..744e8bf2c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -210,7 +210,7 @@ If the test service does not run, or you cannot reach the NetBox home page, some ## Installation -We'll set up a simple HTTP front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we have 2 configurations ready to go - we provide instructions for both [nginx](https://www.nginx.com/resources/wiki/)and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) for service persistence. +We'll set up a simple HTTP front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) for service persistence. ``` # apt-get install gunicorn supervisor @@ -264,7 +264,7 @@ Restart the nginx service to use the new configuration. ``` ## Apache Configuration -If you're feeling adventurous, or you already have Apache installed and can't run a dual-stack on your server - an Apache configuration has been created: +If you're feeling adventurous, or you already have Apache installed and can't run a dual-stack on your server, the following configuration should work for Apache: ``` @@ -279,19 +279,20 @@ If you're feeling adventurous, or you already have Apache installed and can't ru AllowOverride None Order allow,deny Allow from all - #Require all granted [UNCOMMENT THIS IF RUNNING APACHE 2.4] + # Uncomment the line below if running Apache 2.4 + #Require all granted ProxyPass ! - ProxyPass / http://127.0.0.1:8001; - ProxyPassReverse / http://127.0.0.1:8001; + ProxyPass / http://127.0.0.1:8001 + ProxyPassReverse / http://127.0.0.1:8001 ``` -Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf`, add in the newly saved configuration and reload Apache: +Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf` and reload Apache: ``` # a2ensite netbox; service apache2 restart @@ -329,4 +330,3 @@ Finally, restart the supervisor service to detect and run the gunicorn service: At this point, you should be able to connect to the nginx HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running. Please keep in mind that the configurations provided here are a bare minimum to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment. - From cce6c8981031c8ac9cd0f224781c2a788464fef1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 11:57:44 -0400 Subject: [PATCH 23/64] Corrected static path in Apache config --- docs/getting-started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 744e8bf2c..6e9944cd3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -270,9 +270,9 @@ If you're feeling adventurous, or you already have Apache installed and can't ru ProxyPreserveHost On - ServerName netbox.totallycool.tld + ServerName netbox.example.com - Alias /static/ /opt/netbox/static/static + Alias /static/ /opt/netbox/netbox/static Options Indexes FollowSymLinks MultiViews From 6c415794cd4aea385b964be8d5d87b99442775e4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 12:53:43 -0400 Subject: [PATCH 24/64] Corrected description of prefix and VLAN statuses --- docs/ipam.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/ipam.md b/docs/ipam.md index 53c5858f2..ff3810097 100644 --- a/docs/ipam.md +++ b/docs/ipam.md @@ -52,15 +52,13 @@ A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefix ### Statuses -Each prefix is assigned an operational status. This may be one of the following: +Each prefix is assigned an operational status. This is one of the following: * Container - A summary of child prefixes * Active - Provisioned and in use * Reserved - Earmarked for future use * Deprecated - No longer in use -NetBox provides several statuses by default, but you are free to change them to suit the needs of your organization. - ### Roles Whereas a status describes a prefix's operational state, a role describes its function. For example, roles might include: @@ -71,7 +69,7 @@ Whereas a status describes a prefix's operational state, a role describes its fu * Lab * Out-of-band -Role assignment is optional. And like statuses, you are free to create your own. +Role assignment is optional and you are free to create as many as you'd like. --- From ec667eeed0d4f317226cb28541f915612e0bae61 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 13:32:47 -0400 Subject: [PATCH 25/64] Fixes #84: Added IFACE_FF_10GE_COPPER --- .../migrations/0003_auto_20160628_1721.py | 25 +++++++++++++++++++ netbox/dcim/models.py | 6 +++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 netbox/dcim/migrations/0003_auto_20160628_1721.py diff --git a/netbox/dcim/migrations/0003_auto_20160628_1721.py b/netbox/dcim/migrations/0003_auto_20160628_1721.py new file mode 100644 index 000000000..deebc8518 --- /dev/null +++ b/netbox/dcim/migrations/0003_auto_20160628_1721.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-06-28 17:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0002_auto_20160622_1821'), + ] + + operations = [ + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index b6ec28be2..c00a1043b 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -45,14 +45,16 @@ IFACE_FF_VIRTUAL = 0 IFACE_FF_100M_COPPER = 800 IFACE_FF_1GE_COPPER = 1000 IFACE_FF_SFP = 1100 +IFACE_FF_10GE_COPPER = 1150 IFACE_FF_SFP_PLUS = 1200 IFACE_FF_XFP = 1300 IFACE_FF_QSFP_PLUS = 1400 IFACE_FF_CHOICES = [ [IFACE_FF_VIRTUAL, 'Virtual'], - [IFACE_FF_100M_COPPER, '10/100M (Copper)'], - [IFACE_FF_1GE_COPPER, '1GE (Copper)'], + [IFACE_FF_100M_COPPER, '10/100M (100BASE-TX)'], + [IFACE_FF_1GE_COPPER, '1GE (1000BASE-T)'], [IFACE_FF_SFP, '1GE (SFP)'], + [IFACE_FF_10GE_COPPER, '10GE (10GBASE-T)'], [IFACE_FF_SFP_PLUS, '10GE (SFP+)'], [IFACE_FF_XFP, '10GE (XFP)'], [IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'], From fdfc32899d12607821456d7e973d1327fef522b1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 14:10:16 -0400 Subject: [PATCH 26/64] Fixes #75: Ignore a Device's occupied rack units when relocating it within a rack --- netbox/dcim/forms.py | 7 +++++-- netbox/dcim/models.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 776306a07..b158a8349 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -386,10 +386,13 @@ class DeviceForm(forms.ModelForm, BootstrapMixin): # Rack position try: + pk = self.instance.pk if self.instance.pk else None if self.is_bound and self.data.get('rack') and str(self.data.get('face')): - position_choices = Rack.objects.get(pk=self.data['rack']).get_rack_units(face=self.data.get('face')) + position_choices = Rack.objects.get(pk=self.data['rack'])\ + .get_rack_units(face=self.data.get('face'), exclude=pk) elif self.initial.get('rack') and str(self.initial.get('face')): - position_choices = Rack.objects.get(pk=self.initial['rack']).get_rack_units(face=self.initial.get('face')) + position_choices = Rack.objects.get(pk=self.initial['rack'])\ + .get_rack_units(face=self.initial.get('face'), exclude=pk) else: position_choices = [] except Rack.DoesNotExist: diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index c00a1043b..1e5ca1aed 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -215,12 +215,13 @@ class Rack(CreatedUpdatedModel): return "{} ({})".format(self.name, self.facility_id) return self.name - def get_rack_units(self, face=RACK_FACE_FRONT, remove_redundant=False): + def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) + :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack :param remove_redundant: If True, rack units occupied by a device already listed will be omitted """ @@ -231,7 +232,9 @@ class Rack(CreatedUpdatedModel): # Add devices to rack units list if self.pk: for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\ - .filter(rack=self, position__gt=0).filter(Q(face=face) | Q(device_type__is_full_depth=True)): + .exclude(pk=exclude)\ + .filter(rack=self, position__gt=0)\ + .filter(Q(face=face) | Q(device_type__is_full_depth=True)): if remove_redundant: elevation[device.position]['device'] = device for u in range(device.position + 1, device.position + device.device_type.u_height): From 8b357a311d4da557e447b1383ad8db3b3dc1d137 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 14:53:33 -0400 Subject: [PATCH 27/64] Fixes #61: Added list of RackGroups to Site view --- netbox/dcim/views.py | 2 ++ netbox/templates/dcim/site.html | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 50c3243d8..51e508e55 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -75,11 +75,13 @@ def site(request, slug): 'vlan_count': VLAN.objects.filter(site=site).count(), 'circuit_count': Circuit.objects.filter(site=site).count(), } + rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) topology_maps = TopologyMap.objects.filter(site=site) return render(request, 'dcim/site.html', { 'site': site, 'stats': stats, + 'rack_groups': rack_groups, 'topology_maps': topology_maps, }) diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index e6425ecb9..bddc8a505 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -124,6 +124,25 @@
+
+
+ Rack Groups +
+ {% if rack_groups %} + + {% for rg in rack_groups %} + + + + + {% endfor %} +
{{ rg.name }}{{ rg.rack_count }}
+ {% else %} +
+ None +
+ {% endif %} +
Topology Maps @@ -132,7 +151,7 @@ {% for tm in topology_maps %} - + {% endfor %} From e7615cf32fbcfe1375de30225acd729d37451eb0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 15:58:50 -0400 Subject: [PATCH 28/64] Added instructions for upgrading NetBox --- docs/getting-started.md | 53 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 6e9944cd3..6bc8e6a88 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -48,11 +48,12 @@ You can verify that authentication works using the following command: # NetBox -## Dependencies +## Installation + +NetBox requires following dependencies: * python2.7 * python-dev -* git * python-pip * libxml2-dev * libxslt1-dev @@ -65,7 +66,21 @@ You can verify that authentication works using the following command: *graphviz is needed to render topology maps. If you have no need for this feature, graphviz is not required. -## Clone the Git Repository +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 + +Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`. + +``` +# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz +# tar -xzf vX.Y.Z.tar.gz -C /opt +# cd /opt/ +# ln -s netbox-1.0.4/ netbox +# cd /opt/netbox/ +``` + +### Option B: Clone the Git Repository Create the base directory for the NetBox installation. For this guide, we'll use `/opt/netbox`. @@ -74,6 +89,12 @@ Create the base directory for the NetBox installation. For this guide, we'll use # cd /opt/netbox/ ``` +If `git` is not already installed, install it: + +``` +# sudo apt-get install git +``` + Next, clone the NetBox git repository into the current directory: ``` @@ -87,6 +108,8 @@ Resolving deltas: 100% (1495/1495), done. Checking connectivity... done. ``` +### Install Python Packages + Install the necessary Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the required dependencies.) ``` @@ -329,4 +352,26 @@ Finally, restart the supervisor service to detect and run the gunicorn service: At this point, you should be able to connect to the nginx HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running. -Please keep in mind that the configurations provided here are a bare minimum to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment. +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. + +# 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." + +``` +# ./manage.py migrate +``` + +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. + +``` +# ./manage.py collectstatic +``` + +Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`: + +``` +# sudo supervisorctl restart netbox +``` From 4a04af145bc01c909adc52eda6298b00f874ae7b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 16:01:48 -0400 Subject: [PATCH 29/64] Fixed VRF filter for API --- netbox/ipam/api/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 4f9da2657..018e4f366 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,7 +1,7 @@ from rest_framework import generics from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN -from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter +from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter, VRFFilter from . import serializers @@ -12,6 +12,7 @@ class VRFListView(generics.ListAPIView): """ queryset = VRF.objects.all() serializer_class = serializers.VRFSerializer + filter_class = VRFFilter class VRFDetailView(generics.RetrieveAPIView): From 945ca314609bc1de9b861cfa759003430ed133da Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 17:12:09 -0400 Subject: [PATCH 30/64] Fixes #92: Redirect to module creation page on 'add another' --- netbox/dcim/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 51e508e55..1500f096e 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1516,7 +1516,10 @@ def module_add(request, pk): module.device = device module.save() messages.success(request, "Added module {} to {}".format(module.name, module.device.name)) - return redirect('dcim:device_inventory', pk=module.device.pk) + if '_addanother' in request.POST: + return redirect('dcim:module_add', pk=module.device.pk) + else: + return redirect('dcim:device_inventory', pk=module.device.pk) else: form = forms.ModuleForm() From 43e030f1db46709b0d8c0522f0e7502a43d13d2b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 20:21:49 -0400 Subject: [PATCH 31/64] Fixes #83: Corrected example Apache configuration --- docs/getting-started.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 6bc8e6a88..788c3a88f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -287,38 +287,38 @@ Restart the nginx service to use the new configuration. ``` ## Apache Configuration -If you're feeling adventurous, or you already have Apache installed and can't run a dual-stack on your server, the following configuration should work for Apache: +The following configuration should work for Apache. Be sure to modify the `ServerName` appropriately. ``` ProxyPreserveHost On - + ServerName netbox.example.com - Alias /static/ /opt/netbox/netbox/static + Alias /static /opt/netbox/netbox/static Options Indexes FollowSymLinks MultiViews AllowOverride None - Order allow,deny - Allow from all - # Uncomment the line below if running Apache 2.4 - #Require all granted + Require all granted ProxyPass ! - ProxyPass / http://127.0.0.1:8001 - ProxyPassReverse / http://127.0.0.1:8001 + ProxyPass / http://127.0.0.1:8001/ + ProxyPassReverse / http://127.0.0.1:8001/ ``` -Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf` and reload Apache: +Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf`, enable the `proxy` and `proxy_http` modules, and reload Apache: ``` -# a2ensite netbox; service apache2 restart +# a2enmod proxy +# a2enmod proxy_http +# a2ensite netbox +# service apache2 restart ``` ## gunicorn Configuration From b02c54ce527f0aa2c8712ab444d79acd8478ebc4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 23:22:41 -0400 Subject: [PATCH 32/64] A modest attempt at improving interface ordering; see #9 --- netbox/dcim/models.py | 65 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 1e5ca1aed..013ca927b 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -85,6 +85,48 @@ RPC_CLIENT_CHOICES = [ ] +def order_interfaces(queryset, sql_col, primary_ordering=tuple()): + """ + Attempt to match interface names by their slot/position identifiers and order according. Matching is done using the + following pattern: + + {a}/{b}/{c}:{d} + + Interfaces are ordered first by field a, then b, then c, and finally d. Leading text (which typically indicates the + interface's type) is ignored. If any fields are not contained by an interface name, those fields are treated as + None. 'None' is ordered after all other values. For example: + + et-0/0/0 + et-0/0/1 + et-0/1/0 + xe-0/1/1:0 + xe-0/1/1:1 + xe-0/1/1:2 + xe-0/1/1:3 + et-0/1/2 + ... + et-0/1/9 + et-0/1/10 + et-0/1/11 + et-1/0/0 + et-1/0/1 + ... + vlan1 + vlan10 + + :param queryset: The base queryset to be ordered + :param sql_col: Table and name of the SQL column which contains the interface name (ex: ''dcim_interface.name') + :param primary_ordering: A tuple of fields which take ordering precedence before the interface name (optional) + """ + ordering = primary_ordering + ('_id1', '_id2', '_id3', '_id4') + return queryset.extra(select={ + '_id1': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), + '_id2': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), + '_id3': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col), + '_id4': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col), + }).order_by(*ordering) + + class Site(CreatedUpdatedModel): """ A Site represents a geographic location within a network; typically a building or campus. The optional facility @@ -413,6 +455,13 @@ class PowerOutletTemplate(models.Model): return self.name +class InterfaceTemplateManager(models.Manager): + + def get_queryset(self): + qs = super(InterfaceTemplateManager, self).get_queryset() + return order_interfaces(qs, 'dcim_interfacetemplate.name', ('device_type',)) + + class InterfaceTemplate(models.Model): """ A template for a physical data interface on a new Device. @@ -422,6 +471,8 @@ class InterfaceTemplate(models.Model): form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS) mgmt_only = models.BooleanField(default=False, verbose_name='Management only') + objects = InterfaceTemplateManager() + class Meta: ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] @@ -713,18 +764,8 @@ class PowerOutlet(models.Model): class InterfaceManager(models.Manager): def get_queryset(self): - """ - Cast up to three interface slot/position IDs as independent integers and order appropriately. This ensures that - interfaces are ordered numerically without regard to type. For example: - xe-0/0/0, xe-0/0/1, xe-0/0/2 ... et-0/0/47, et-0/0/48, et-0/0/49 ... - instead of: - et-0/0/48, et-0/0/49, et-0/0/50 ... et-0/0/53, xe-0/0/0, xe-0/0/1 ... - """ - return super(InterfaceManager, self).get_queryset().extra(select={ - '_id1': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)\/([0-9]+)$') AS integer)", - '_id2': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)$') AS integer)", - '_id3': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)$') AS integer)", - }).order_by('device', '_id1', '_id2', '_id3') + qs = super(InterfaceManager, self).get_queryset() + return order_interfaces(qs, 'dcim_interface.name', ('device',)) def virtual(self): return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL) From 522a0c20e7a0c13a7a675ad67be1751613bd307d Mon Sep 17 00:00:00 2001 From: Pit Kleyersburg Date: Wed, 29 Jun 2016 11:25:36 +0200 Subject: [PATCH 33/64] 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 2436de401..81c4e9170 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 8987863a5..c1afc1b21 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 4ed3d545669082066b7eea890bae07cc6ab3613e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 09:45:59 -0400 Subject: [PATCH 34/64] Fixes #103: Corrected VRF filters for Prefixes and IPAddresses --- netbox/ipam/filters.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 8aab11475..56e2cbdd2 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -46,9 +46,14 @@ class PrefixFilter(django_filters.FilterSet): action='search_by_parent', label='Parent prefix', ) + vrf = django_filters.MethodFilter( + action='_vrf', + label='VRF', + ) + # Duplicate of `vrf` for backward-compatibility vrf_id = django_filters.MethodFilter( - action='vrf', - label='VRF (ID)', + action='_vrf', + label='VRF', ) site_id = django_filters.ModelMultipleChoiceFilter( name='site', @@ -84,7 +89,7 @@ class PrefixFilter(django_filters.FilterSet): class Meta: model = Prefix - fields = ['family', 'site_id', 'site', 'vrf_id', 'vrf', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role'] + fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role'] def search(self, queryset, value): value = value.strip() @@ -104,7 +109,7 @@ class PrefixFilter(django_filters.FilterSet): except AddrFormatError: return queryset.none() - def vrf(self, queryset, value): + def _vrf(self, queryset, value): if str(value) == '': return queryset try: @@ -121,10 +126,14 @@ class IPAddressFilter(django_filters.FilterSet): action='search', label='Search', ) - vrf_id = django_filters.ModelMultipleChoiceFilter( - name='vrf', - queryset=VRF.objects.all(), - label='VRF (ID)', + vrf = django_filters.MethodFilter( + action='_vrf', + label='VRF', + ) + # Duplicate of `vrf` for backward-compatibility + vrf_id = django_filters.MethodFilter( + action='_vrf', + label='VRF', ) device_id = django_filters.ModelMultipleChoiceFilter( name='interface__device', @@ -155,6 +164,17 @@ class IPAddressFilter(django_filters.FilterSet): except AddrFormatError: return queryset.none() + def _vrf(self, queryset, value): + if str(value) == '': + return queryset + try: + vrf_id = int(value) + except ValueError: + return queryset.none() + if vrf_id == 0: + return queryset.filter(vrf__isnull=True) + return queryset.filter(vrf__pk=value) + class VLANFilter(django_filters.FilterSet): site_id = django_filters.ModelMultipleChoiceFilter( From aa000bf26d947877fc8abd67e435d68e13fdbc3a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 10:52:06 -0400 Subject: [PATCH 35/64] 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 616f628d6..0c7a411cd 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 36/64] 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 a2e0fba96..4f65eae90 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 37/64] 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 47f22168a..a0cb92f4d 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 1500f096e..3cf157c7d 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 bddc8a505..a9db55e19 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 38/64] 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 a0cb92f4d..9712899a8 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 39/64] 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 16bba4558..fed0c2fd7 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 a67d3c440..1d5fa1ad5 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 40/64] 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 e9eebeeae..db048c39d 100644 --- a/netbox/templates/dcim/device_import.html +++ b/netbox/templates/dcim/device_import.html @@ -74,12 +74,12 @@
- +
{{ tm }} {{ tm }} {{ tm.description }}
Face Rack face; front or rear (optional)rearRear

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 41/64] 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 788c3a88f..3b379a218 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 42/64] 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 260365eec..9855b9273 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 43/64] 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 b158a8349..e9b2f9dac 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 013ca927b..aeee10ee5 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 44/64] 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 7628f9af7..e769694ea 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 788c3a88f..592f658c6 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 000000000..f2f1a5f28 --- /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 45/64] 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 ab739f9e3..cc563c284 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 46/64] 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 592f658c6..b489567a4 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 From 519ab21ba008b22ba06b3b2d7a570dc86613bd94 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 17:48:11 -0400 Subject: [PATCH 47/64] Version bump for next release --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index fed0c2fd7..7e4a22378 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -11,7 +11,7 @@ except ImportError: "the documentation.") -VERSION = '1.0.6' +VERSION = '1.0.7' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From 019daf55248d6d74bea37f4a2c0a6f8d1304e7cc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 22:51:10 -0400 Subject: [PATCH 48/64] Fixes #135: Add button to toggle navbar on small screens --- netbox/templates/_base.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 1d5fa1ad5..9feb87bde 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -13,6 +13,12 @@