mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-24 18:58:15 +01:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c93bc40479 | ||
|
|
4ed3d54566 | ||
|
|
b02c54ce52 | ||
|
|
43e030f1db | ||
|
|
945ca31460 | ||
|
|
fc3cb72ab8 | ||
|
|
4a04af145b | ||
|
|
e7615cf32f | ||
|
|
8b357a311d | ||
|
|
fdfc32899d | ||
|
|
03fa000d8d | ||
|
|
ec667eeed0 | ||
|
|
6c415794cd | ||
|
|
2ddb4b90c5 | ||
|
|
cce6c89810 | ||
|
|
b37503ed8f | ||
|
|
374702927b | ||
|
|
0eb8227044 | ||
|
|
98febf3979 | ||
|
|
6a4a636794 | ||
|
|
9acd0e99f9 | ||
|
|
f1857dd189 | ||
|
|
d22e4e7698 | ||
|
|
6848a3dc81 | ||
|
|
4dac43c1c9 | ||
|
|
b392aa4a4a | ||
|
|
5181c97281 | ||
|
|
66a16dd06b | ||
|
|
c5d498ac14 | ||
|
|
2080abc6c3 | ||
|
|
b379918295 | ||
|
|
4e5f537cc5 | ||
|
|
7918f85cdd | ||
|
|
df1147d941 | ||
|
|
65bc91e9de | ||
|
|
9aa0972a8c | ||
|
|
4cd6f99cbd | ||
|
|
df01947c9e | ||
|
|
4dd31497e5 | ||
|
|
f958bc0580 | ||
|
|
0a22821209 | ||
|
|
a4cbfd7d5b | ||
|
|
f0fb60734a | ||
|
|
e334c64a7c | ||
|
|
6e068770ea | ||
|
|
d5d4eb9fd5 | ||
|
|
ab880e1053 | ||
|
|
1ea8f04c23 | ||
|
|
0b37d4f5e6 | ||
|
|
c6e66a073d | ||
|
|
eade3cbd6b | ||
|
|
7cf437e11b | ||
|
|
19a302774a | ||
|
|
a35d927235 | ||
|
|
1cd20861f2 | ||
|
|
e78263637a | ||
|
|
215c31e7a0 | ||
|
|
5935a8843e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
*.pyc
|
||||
configuration.py
|
||||
.idea
|
||||
*.sh
|
||||
/*.sh
|
||||
fabfile.py
|
||||
|
||||
|
||||
8
.travis.yml
Normal file
8
.travis.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install pep8
|
||||
script:
|
||||
- ./scripts/cibuild.sh
|
||||
@@ -48,3 +48,15 @@ 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.
|
||||
|
||||
* 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
|
||||
|
||||
19
README.md
19
README.md
@@ -1,7 +1,26 @@
|
||||
# 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).
|
||||
|
||||
Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net**!
|
||||
|
||||
### Build Status
|
||||
|
||||
| | python 2.7 |
|
||||
|-------------|------------|
|
||||
| **master** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
| **develop** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# Installation
|
||||
|
||||
Please see docs/getting-started.md for instructions on installing NetBox.
|
||||
|
||||
@@ -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.)
|
||||
|
||||
```
|
||||
@@ -206,20 +229,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 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 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,13 +285,48 @@ Restart the nginx service to use the new configuration.
|
||||
# service nginx restart
|
||||
* Restarting nginx nginx
|
||||
```
|
||||
## Apache Configuration
|
||||
|
||||
The following configuration should work for Apache. Be sure to modify the `ServerName` appropriately.
|
||||
|
||||
```
|
||||
<VirtualHost *:80>
|
||||
ProxyPreserveHost On
|
||||
|
||||
ServerName netbox.example.com
|
||||
|
||||
Alias /static /opt/netbox/netbox/static
|
||||
|
||||
<Directory /opt/netbox/netbox/static>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Location /static>
|
||||
ProxyPass !
|
||||
</Location>
|
||||
|
||||
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`, enable the `proxy` and `proxy_http` modules, and reload Apache:
|
||||
|
||||
```
|
||||
# a2enmod proxy
|
||||
# a2enmod proxy_http
|
||||
# a2ensite netbox
|
||||
# service apache2 restart
|
||||
```
|
||||
|
||||
## gunicorn Configuration
|
||||
|
||||
Save the following configuration file in the root netbox installation path (in this example, `/opt/netbox/`.) as `gunicorn_config.py`. Be sure to update the `pythonpath` variable if needed.
|
||||
Save the following configuration file in the root netbox installation path (in this example, `/opt/netbox/`.) as `gunicorn_config.py`. Be sure to verify the location of the gunicorn executable (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed.
|
||||
|
||||
```
|
||||
command = '/usr/local/bin/gunicorn'
|
||||
command = '/usr/bin/gunicorn'
|
||||
pythonpath = '/opt/netbox/netbox'
|
||||
bind = '127.0.0.1:8001'
|
||||
workers = 3
|
||||
@@ -288,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
|
||||
```
|
||||
|
||||
10
docs/ipam.md
10
docs/ipam.md
@@ -32,11 +32,13 @@ 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.
|
||||
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
BIN
docs/screenshot1.png
Normal file
BIN
docs/screenshot1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/screenshot2.png
Normal file
BIN
docs/screenshot2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
BIN
docs/screenshot3.png
Normal file
BIN
docs/screenshot3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@@ -47,6 +47,12 @@ class RackFilter(django_filters.FilterSet):
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
|
||||
@@ -138,24 +138,23 @@ class RackForm(forms.ModelForm, BootstrapMixin):
|
||||
class RackFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Group not found.'})
|
||||
group_name = forms.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'u_height']
|
||||
fields = ['site', 'group_name', 'name', 'facility_id', 'u_height']
|
||||
|
||||
def clean(self):
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
group = self.cleaned_data.get('group')
|
||||
group = self.cleaned_data.get('group_name')
|
||||
|
||||
# Validate device type
|
||||
# Validate rack group
|
||||
if site and group:
|
||||
try:
|
||||
self.instance.group = RackGroup.objects.get(site=site, name=group)
|
||||
except RackGroup.DoesNotExist:
|
||||
self.add_error('group', "Invalid rack group ({})".format(group))
|
||||
self.add_error('group_name', "Invalid rack group ({})".format(group))
|
||||
|
||||
|
||||
class RackImportForm(BulkImportForm, BootstrapMixin):
|
||||
@@ -387,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:
|
||||
@@ -425,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
|
||||
@@ -1037,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:
|
||||
|
||||
25
netbox/dcim/migrations/0003_auto_20160628_1721.py
Normal file
25
netbox/dcim/migrations/0003_auto_20160628_1721.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
@@ -183,6 +227,17 @@ class Rack(CreatedUpdatedModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that Rack is tall enough to house the installed Devices
|
||||
if self.pk:
|
||||
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:
|
||||
raise ValidationError("Rack must be at least {}U tall with currently installed devices."
|
||||
.format(min_height))
|
||||
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
self.site.name,
|
||||
@@ -202,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
|
||||
"""
|
||||
|
||||
@@ -218,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):
|
||||
@@ -397,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.
|
||||
@@ -406,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']
|
||||
@@ -697,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)
|
||||
|
||||
@@ -47,7 +47,7 @@ class SiteTest(APITestCase):
|
||||
graph_fields = [
|
||||
'name',
|
||||
'embed_url',
|
||||
'link',
|
||||
'embed_link',
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/sites/'):
|
||||
|
||||
@@ -64,7 +64,7 @@ class RackTestCase(TestCase):
|
||||
rack=rack1,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
)
|
||||
device1.save()
|
||||
|
||||
# Validate rack height
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -353,7 +355,7 @@ class ComponentTemplateCreateView(View):
|
||||
|
||||
if not form.errors:
|
||||
self.model.objects.bulk_create(component_templates)
|
||||
messages.success(request, "Added {} compontent(s) to {}".format(len(component_templates), devicetype))
|
||||
messages.success(request, "Added {} component(s) to {}".format(len(component_templates), devicetype))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
else:
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ body {
|
||||
padding-top: 70px;
|
||||
}
|
||||
.container {
|
||||
width: 1340px;
|
||||
width: auto;
|
||||
max-width: 1340px;
|
||||
}
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
{% if settings.MAINTENANCE_MODE %}
|
||||
<div class="alert alert-warning text-center" role="alert">
|
||||
<h4><i class="fa fa-exclamation-triangle"></i> Maintenance Mode</h4>
|
||||
<p>The application is currently in maintenance mode.</p>
|
||||
<p>NetBox is currently in maintenance mode. Functionality may be limited.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for message in messages %}
|
||||
@@ -246,7 +246,7 @@
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="{% url 'docs_root' %}">Docs</a> ·
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="/api/docs/">API</a> ·
|
||||
<i class="fa fa-fw fa-code text-primary"></i><a href="https://github.com/digitalocean/netbox">Code</a>
|
||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -119,31 +119,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Parent Prefixes</strong>
|
||||
</div>
|
||||
{% if parent_prefixes %}
|
||||
<table class="table table-hover panel-body">
|
||||
{% for p in parent_prefixes %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'ipam:prefix' pk=p.pk %}">{{ p }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if p.site %}
|
||||
<a href="{% url 'dcim:site' slug=p.site.slug %}">{{ p.site }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ p.status }}</td>
|
||||
<td>{{ p.role }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">None</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
@@ -19,11 +19,11 @@ def expand_pattern(string):
|
||||
lead, pattern, remnant = re.split(EXPANSION_PATTERN, string, maxsplit=1)
|
||||
x, y = pattern.split('-')
|
||||
for i in range(int(x), int(y) + 1):
|
||||
if remnant:
|
||||
if re.search(EXPANSION_PATTERN, remnant):
|
||||
for string in expand_pattern(remnant):
|
||||
yield "{}{}{}".format(lead, i, string)
|
||||
else:
|
||||
yield "{}{}".format(lead, i)
|
||||
yield "{}{}{}".format(lead, i, remnant)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
|
||||
|
||||
52
scripts/cibuild.sh
Executable file
52
scripts/cibuild.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/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
|
||||
|
||||
# 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."
|
||||
|
||||
exit $EXIT
|
||||
Reference in New Issue
Block a user