Compare commits

..

30 Commits

Author SHA1 Message Date
Jeremy Stretch
c93bc40479 Merge pull request #106 from digitalocean/develop
Release v1.0.5
2016-06-29 10:00:46 -04:00
Jeremy Stretch
4ed3d54566 Fixes #103: Corrected VRF filters for Prefixes and IPAddresses 2016-06-29 09:45:59 -04:00
Jeremy Stretch
b02c54ce52 A modest attempt at improving interface ordering; see #9 2016-06-28 23:22:41 -04:00
Jeremy Stretch
43e030f1db Fixes #83: Corrected example Apache configuration 2016-06-28 20:21:49 -04:00
Jeremy Stretch
945ca31460 Fixes #92: Redirect to module creation page on 'add another' 2016-06-28 17:12:09 -04:00
Jeremy Stretch
fc3cb72ab8 Merge pull request #82 from digitalocean/contributing-checklist
Add sanity check checklist for submitting pull requests
2016-06-28 16:26:20 -04:00
Jeremy Stretch
4a04af145b Fixed VRF filter for API 2016-06-28 16:01:48 -04:00
Jeremy Stretch
e7615cf32f Added instructions for upgrading NetBox 2016-06-28 15:58:50 -04:00
Jeremy Stretch
8b357a311d Fixes #61: Added list of RackGroups to Site view 2016-06-28 14:53:33 -04:00
Jeremy Stretch
fdfc32899d Fixes #75: Ignore a Device's occupied rack units when relocating it within a rack 2016-06-28 14:10:16 -04:00
Jeremy Stretch
03fa000d8d Merge pull request #86 from digitalocean/iface_form_factors
Fixes #84: Added IFACE_FF_10GE_COPPER
2016-06-28 13:38:51 -04:00
Jeremy Stretch
ec667eeed0 Fixes #84: Added IFACE_FF_10GE_COPPER 2016-06-28 13:32:47 -04:00
Jeremy Stretch
6c415794cd Corrected description of prefix and VLAN statuses 2016-06-28 12:53:43 -04:00
Jeremy Stretch
2ddb4b90c5 Merge pull request #85 from digitalocean/develop
Release v1.0.4
2016-06-28 12:17:12 -04:00
Jeremy Stretch
cce6c89810 Corrected static path in Apache config 2016-06-28 11:57:44 -04:00
Jeremy Stretch
b37503ed8f Corrected typos in the Apache config; cleaned up grammar 2016-06-28 11:50:25 -04:00
Jeremy Stretch
374702927b Fixes #80: Correct rack face (lowercase) to be consistent with export behavior (uppercase) 2016-06-28 11:38:09 -04:00
Jeremy Stretch
0eb8227044 Merge branch 'develop' of https://github.com/digitalocean/netbox into develop 2016-06-28 11:15:56 -04:00
Jeremy Stretch
98febf3979 Fixes #72: Check for re-used interfaces when importing interface connections 2016-06-28 11:11:53 -04:00
Jeremy Stretch
6a4a636794 Merge pull request #58 from digitalocean/travis-ci-pep-8
Add CI check for PEP 8 compliance
2016-06-28 10:56:42 -04:00
Matt Layher
9acd0e99f9 Add sanity check checklist for submitting pull requests 2016-06-28 10:55:38 -04:00
Matt Layher
f1857dd189 Add CI check for PEP 8 compliance 2016-06-28 10:43:38 -04:00
Jeremy Stretch
d22e4e7698 Merge pull request #59 from digitalocean/dcim-tests-pep-8
Fix PEP 8 error in DCIM tests
2016-06-28 10:42:38 -04:00
Jeremy Stretch
6848a3dc81 Fixes #67: Improved Aggregate validation; extended aggregate documentation 2016-06-28 10:04:03 -04:00
Jeremy Stretch
4dac43c1c9 Fixes #48: Set .container to auto with a max width 2016-06-28 09:50:00 -04:00
Jeremy Stretch
b392aa4a4a Fixes #45: Strip plus signs during slugification 2016-06-28 09:39:55 -04:00
Matt Layher
5181c97281 Fix PEP 8 error in DCIM tests 2016-06-28 00:25:12 -04:00
Jeremy Stretch
66a16dd06b Merge pull request #41 from digitalocean/travis-ci-tests
Run tests in CI
2016-06-28 00:10:19 -04:00
Matt Layher
c5d498ac14 Run tests in CI 2016-06-28 00:05:18 -04:00
Jeremy Stretch
2080abc6c3 Corrected SiteTest to account for earlier Graph model change 2016-06-27 23:56:39 -04:00
18 changed files with 276 additions and 57 deletions

View File

@@ -3,5 +3,6 @@ python:
- "2.7"
install:
- pip install -r requirements.txt
- pip install pep8
script:
- ./scripts/cibuild.sh

View File

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

View File

@@ -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.)
```
@@ -210,7 +233,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,37 +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 - an Apache configuration has been created:
The following configuration should work for Apache. Be sure to modify the `ServerName` appropriately.
```
<VirtualHost *:80>
ProxyPreserveHost On
ServerName netbox.totallycool.tld
Alias /static/ /opt/netbox/static/static
ServerName netbox.example.com
Alias /static /opt/netbox/netbox/static
<Directory /opt/netbox/netbox/static>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
Allow from all
#Require all granted [UNCOMMENT THIS IF RUNNING APACHE 2.4]
Require all granted
</Directory>
<Location /static>
ProxyPass !
</Location>
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/
</VirtualHost>
```
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`, 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
@@ -328,5 +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
```

View File

@@ -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.
@@ -50,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:
@@ -69,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.
---

View File

@@ -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:
@@ -424,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.ChoiceField(choices=[('Front', 'Front'), ('Rear', 'Rear')])
class Meta:
model = Device
@@ -1036,20 +1039,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:

View File

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

View File

@@ -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+)'],
@@ -83,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
@@ -213,12 +257,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
"""
@@ -229,7 +274,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):
@@ -408,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.
@@ -417,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']
@@ -708,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)

View File

@@ -47,7 +47,7 @@ class SiteTest(APITestCase):
graph_fields = [
'name',
'embed_url',
'link',
'embed_link',
]
def test_get_list(self, endpoint='/api/dcim/sites/'):

View File

@@ -64,7 +64,7 @@ class RackTestCase(TestCase):
rack=rack1,
position=10,
face=RACK_FACE_REAR,
)
)
device1.save()
# Validate rack height

View File

@@ -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,
})
@@ -1514,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()

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,8 @@ body {
padding-top: 70px;
}
.container {
width: 1340px;
width: auto;
max-width: 1340px;
}
.wrapper {
min-height: 100%;

View File

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

View File

@@ -8,6 +8,14 @@
<h1>Interface Connections Import</h1>
<div class="row">
<div class="col-md-6">
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
<form action="." method="post" class="form">
{% csrf_token %}
{% render_form form %}

View File

@@ -124,6 +124,25 @@
</tr>
</table>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Rack Groups</strong>
</div>
{% if rack_groups %}
<table class="table table-hover panel-body">
{% for rg in rack_groups %}
<tr>
<td><i class="fa fa-fw fa-folder"></i> <a href="{{ rg.get_absolute_url }}">{{ rg.name }}</a></td>
<td>{{ rg.rack_count }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="panel-body text-muted">
None
</div>
{% endif %}
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Topology Maps</strong>
@@ -132,7 +151,7 @@
<table class="table table-hover panel-body">
{% for tm in topology_maps %}
<tr>
<td><i class="fa fa-fw fa-map text-success"></i> <a href="{% url 'dcim-api:topology_map' slug=tm.slug %}" target="_blank">{{ tm }}</a></td>
<td><i class="fa fa-fw fa-map"></i> <a href="{% url 'dcim-api:topology_map' slug=tm.slug %}" target="_blank">{{ tm }}</a></td>
<td>{{ tm.description }}</td>
</tr>
{% endfor %}

View File

@@ -21,6 +21,30 @@ 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
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."