mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 02:48:38 -06:00
This commit is contained in:
commit
282097c7e8
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
configuration.py
|
configuration.py
|
||||||
.idea
|
.idea
|
||||||
*.sh
|
/*.sh
|
||||||
|
!upgrade.sh
|
||||||
fabfile.py
|
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,19 @@ 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 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)
|
* A rough description of any changes necessary to the database schema (if applicable)
|
||||||
* Any third-party libraries or other resources which would be involved
|
* Any third-party libraries or other resources which would be involved
|
||||||
|
|
||||||
|
## Submitting Pull Requests
|
||||||
|
|
||||||
|
* Be sure to open an issue before starting work on a pull request, and discuss your idea with the NetBox maintainers
|
||||||
|
before beginning work. This will help prevent wasting time on something that might we might not be able to implement.
|
||||||
|
When suggesting a new feature, also make sure it won't conflict with any work that's already in progress.
|
||||||
|
|
||||||
|
* 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
|
||||||
|
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM ubuntu:14.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python2.7 \
|
||||||
|
python-dev \
|
||||||
|
git \
|
||||||
|
python-pip \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
libffi-dev \
|
||||||
|
graphviz \
|
||||||
|
libpq-dev \
|
||||||
|
build-essential \
|
||||||
|
gunicorn \
|
||||||
|
--no-install-recommends \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& mkdir -p /opt/netbox \
|
||||||
|
&& cd /opt/netbox \
|
||||||
|
&& git clone --depth 1 https://github.com/digitalocean/netbox.git -b master . \
|
||||||
|
&& pip install -r requirements.txt \
|
||||||
|
&& apt-get purge -y --auto-remove git build-essential
|
||||||
|
|
||||||
|
ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/docker-entrypoint.sh" ]
|
15
README.md
15
README.md
@ -1,7 +1,20 @@
|
|||||||
|
# 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 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).
|
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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
@ -12,6 +25,8 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com
|
|||||||
|
|
||||||
Please see docs/getting-started.md for instructions on installing NetBox.
|
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
|
# Components
|
||||||
|
|
||||||
NetBox understands all of the physical and logical building blocks that comprise network infrastructure, and the manners in which they are all related.
|
NetBox understands all of the physical and logical building blocks that comprise network infrastructure, and the manners in which they are all related.
|
||||||
|
55
docker-compose.yml
Normal file
55
docker-compose.yml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:9.6
|
||||||
|
container_name: postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: netbox
|
||||||
|
POSTGRES_PASSWORD: J5brHrAXFLQSif0K
|
||||||
|
POSTGRES_DB: netbox
|
||||||
|
netbox:
|
||||||
|
build: .
|
||||||
|
links:
|
||||||
|
- postgres
|
||||||
|
container_name: netbox
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
SUPERUSER_NAME: admin
|
||||||
|
SUPERUSER_EMAIL: admin@example.com
|
||||||
|
SUPERUSER_PASSWORD: admin
|
||||||
|
ALLOWED_HOSTS: localhost
|
||||||
|
DB_NAME: netbox
|
||||||
|
DB_USER: netbox
|
||||||
|
DB_PASSWORD: J5brHrAXFLQSif0K
|
||||||
|
DB_HOST: postgres
|
||||||
|
SECRET_KEY: r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
|
||||||
|
EMAIL_SERVER: localhost
|
||||||
|
EMAIL_PORT: 25
|
||||||
|
EMAIL_USERNAME: foo
|
||||||
|
EMAIL_PASSWORD: bar
|
||||||
|
EMAIL_TIMEOUT: 10
|
||||||
|
EMAIL_FROM: netbox@bar.com
|
||||||
|
NETBOX_USERNAME: guest
|
||||||
|
NETBOX_PASSWORD: guest
|
||||||
|
volumes:
|
||||||
|
- $PWD/netbox/netbox/configuration.docker.py:/opt/netbox/netbox/netbox/configuration.py:ro
|
||||||
|
- $PWD/docker/gunicorn_config.py:/opt/netbox/gunicorn_config.py:ro
|
||||||
|
- netbox-static-files:/opt/netbox/netbox/static
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.11.1-alpine
|
||||||
|
links:
|
||||||
|
- netbox
|
||||||
|
container_name: nginx
|
||||||
|
depends_on:
|
||||||
|
- netbox
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
volumes:
|
||||||
|
- $PWD/docker/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
volumes_from:
|
||||||
|
- netbox
|
||||||
|
volumes:
|
||||||
|
netbox-static-files:
|
||||||
|
driver: local
|
22
docker/docker-entrypoint.sh
Executable file
22
docker/docker-entrypoint.sh
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# run db migrations (retry on error)
|
||||||
|
while ! /opt/netbox/netbox/manage.py migrate 2>&1; do
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
# create superuser silently
|
||||||
|
if [[ -z ${SUPERUSER_NAME} || -z ${SUPERUSER_EMAIL} || -z ${SUPERUSER_PASSWORD} ]]; then
|
||||||
|
SUPERUSER_NAME='admin'
|
||||||
|
SUPERUSER_EMAIL='admin@example.com'
|
||||||
|
SUPERUSER_PASSWORD='admin'
|
||||||
|
echo "Using defaults: Username: ${SUPERUSER_NAME}, E-Mail: ${SUPERUSER_EMAIL}, Password: ${SUPERUSER_PASSWORD}"
|
||||||
|
fi
|
||||||
|
echo "from django.contrib.auth.models import User; User.objects.create_superuser('${SUPERUSER_NAME}', '${SUPERUSER_EMAIL}', '${SUPERUSER_PASSWORD}')" | python /opt/netbox/netbox/manage.py shell
|
||||||
|
|
||||||
|
# copy static files
|
||||||
|
/opt/netbox/netbox/manage.py collectstatic --no-input
|
||||||
|
|
||||||
|
# start unicorn
|
||||||
|
gunicorn --log-level debug --debug --error-logfile /dev/stderr --log-file /dev/stdout -c /opt/netbox/gunicorn_config.py netbox.wsgi
|
5
docker/gunicorn_config.py
Normal file
5
docker/gunicorn_config.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
command = '/usr/bin/gunicorn'
|
||||||
|
pythonpath = '/opt/netbox/netbox'
|
||||||
|
bind = '0.0.0.0:8001'
|
||||||
|
workers = 3
|
||||||
|
user = 'root'
|
35
docker/nginx.conf
Normal file
35
docker/nginx.conf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
gzip on;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /opt/netbox/netbox/static/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://netbox:8001;
|
||||||
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -59,12 +59,12 @@ Note that assignment of components from templates occurs only at the time of dev
|
|||||||
|
|
||||||
# Devices
|
# Devices
|
||||||
|
|
||||||
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and whether they are full depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
|
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
|
||||||
|
|
||||||
|
When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 shows as being mounted in U8.
|
||||||
|
|
||||||
A device is said to be "full depth" if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede air flow.
|
A device is said to be "full depth" if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede air flow.
|
||||||
|
|
||||||
Each device has a physical device type (make and model), which is discussed below.
|
|
||||||
|
|
||||||
### Roles
|
### Roles
|
||||||
|
|
||||||
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, device can only belong to one device role.
|
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, device can only belong to one device role.
|
||||||
|
54
docs/getting-started-docker.md
Normal file
54
docs/getting-started-docker.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<h1>Getting Started with NetBox and Docker</h1>
|
||||||
|
|
||||||
|
This guide assumes that the latest versions of [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) are already installed in your host.
|
||||||
|
|
||||||
|
# Quickstart
|
||||||
|
|
||||||
|
To get NetBox up and running:
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/digitalocean/netbox.git
|
||||||
|
cd netbox
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available on http://localhost/ after a few minutes.
|
||||||
|
|
||||||
|
Default credentials:
|
||||||
|
* user: admin
|
||||||
|
* password: admin
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
You can configure the app at runtime using variables (see docker-compose.yml).
|
||||||
|
|
||||||
|
Possible environment variables:
|
||||||
|
|
||||||
|
* SUPERUSER_NAME
|
||||||
|
* SUPERUSER_EMAIL
|
||||||
|
* SUPERUSER_PASSWORD
|
||||||
|
* ALLOWED_HOSTS
|
||||||
|
* DB_NAME
|
||||||
|
* DB_USER
|
||||||
|
* DB_PASSWORD
|
||||||
|
* DB_HOST
|
||||||
|
* DB_PORT
|
||||||
|
* SECRET_KEY
|
||||||
|
* EMAIL_SERVER
|
||||||
|
* EMAIL_PORT
|
||||||
|
* EMAIL_USERNAME
|
||||||
|
* EMAIL_PASSWORD
|
||||||
|
* EMAIL_TIMEOUT
|
||||||
|
* EMAIL_FROM
|
||||||
|
* LOGIN_REQUIRED
|
||||||
|
* MAINTENANCE_MODE
|
||||||
|
* NETBOX_USERNAME
|
||||||
|
* NETBOX_PASSWORD
|
||||||
|
* PAGINATE_COUNT
|
||||||
|
* TIME_ZONE
|
||||||
|
* DATE_FORMAT
|
||||||
|
* SHORT_DATE_FORMAT
|
||||||
|
* TIME_FORMAT
|
||||||
|
* SHORT_TIME_FORMAT
|
||||||
|
* DATETIME_FORMAT
|
||||||
|
* SHORT_DATETIME_FORMAT
|
||||||
|
|
@ -15,7 +15,7 @@ The following packages are needed to install PostgreSQL:
|
|||||||
* python-psycopg2
|
* python-psycopg2
|
||||||
|
|
||||||
```
|
```
|
||||||
# apt-get install postgresql libpq-dev python-psycopg2
|
# sudo apt-get install -y postgresql libpq-dev python-psycopg2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@ -48,24 +48,37 @@ You can verify that authentication works using the following command:
|
|||||||
|
|
||||||
# NetBox
|
# NetBox
|
||||||
|
|
||||||
## Dependencies
|
## Installation
|
||||||
|
|
||||||
|
NetBox requires following dependencies:
|
||||||
|
|
||||||
* python2.7
|
* python2.7
|
||||||
* python-dev
|
* python-dev
|
||||||
* git
|
|
||||||
* python-pip
|
* python-pip
|
||||||
* libxml2-dev
|
* libxml2-dev
|
||||||
* libxslt1-dev
|
* libxslt1-dev
|
||||||
* libffi-dev
|
* libffi-dev
|
||||||
* graphviz*
|
* graphviz
|
||||||
|
|
||||||
```
|
```
|
||||||
# apt-get install python2.7 python-dev git python-pip libxml2-dev libxslt1-dev libffi-dev graphviz
|
# sudo apt-get install -y 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.
|
||||||
|
|
||||||
## Clone the Git Repository
|
### 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`.
|
Create the base directory for the NetBox installation. For this guide, we'll use `/opt/netbox`.
|
||||||
|
|
||||||
@ -74,10 +87,16 @@ Create the base directory for the NetBox installation. For this guide, we'll use
|
|||||||
# cd /opt/netbox/
|
# cd /opt/netbox/
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, clone the NetBox git repository into the current directory:
|
If `git` is not already installed, install it:
|
||||||
|
|
||||||
```
|
```
|
||||||
# git clone https://github.com/digitalocean/netbox.git .
|
# sudo apt-get install -y git
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
# git clone -b master https://github.com/digitalocean/netbox.git .
|
||||||
Cloning into '.'...
|
Cloning into '.'...
|
||||||
remote: Counting objects: 1994, done.
|
remote: Counting objects: 1994, done.
|
||||||
remote: Compressing objects: 100% (150/150), done.
|
remote: Compressing objects: 100% (150/150), done.
|
||||||
@ -87,10 +106,12 @@ Resolving deltas: 100% (1495/1495), done.
|
|||||||
Checking connectivity... 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.)
|
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.)
|
||||||
|
|
||||||
```
|
```
|
||||||
# pip install -r requirements.txt
|
# sudo pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@ -145,6 +166,7 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
|
|||||||
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# cd /opt/netbox/netbox/
|
||||||
# ./manage.py migrate
|
# ./manage.py migrate
|
||||||
Operations to perform:
|
Operations to perform:
|
||||||
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
|
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
|
||||||
@ -206,20 +228,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.
|
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
|
## 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
|
# sudo apt-get install -y gunicorn supervisor
|
||||||
```
|
```
|
||||||
|
|
||||||
## nginx Configuration
|
## nginx Configuration
|
||||||
|
|
||||||
The following will serve as a minimal nginx configuration. Be sure to modify your server name and installation path appropriately.
|
The following will serve as a minimal nginx configuration. Be sure to modify your server name and installation path appropriately.
|
||||||
|
|
||||||
|
```
|
||||||
|
# sudo apt-get install -y nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Once nginx is installed, proceed with the following configuration:
|
||||||
|
|
||||||
```
|
```
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
@ -256,10 +284,49 @@ Restart the nginx service to use the new configuration.
|
|||||||
# service nginx restart
|
# service nginx restart
|
||||||
* Restarting nginx nginx
|
* Restarting nginx nginx
|
||||||
```
|
```
|
||||||
|
## Apache Configuration
|
||||||
|
|
||||||
|
```
|
||||||
|
# sudo apt-get install -y apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
Once Apache is installed, proceed with the following configuration (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
|
## 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 verify the location of the gunicorn executable (e.g. `which gunicorn`) and 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/bin/gunicorn'
|
command = '/usr/bin/gunicorn'
|
||||||
@ -288,4 +355,103 @@ 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.
|
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.
|
||||||
|
|
||||||
|
## 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 install -y bc
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we'll clone Let's Encrypt into /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 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).
|
||||||
|
|
||||||
|
```
|
||||||
|
# ./upgrade.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script:
|
||||||
|
|
||||||
|
* 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`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# 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.
|
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
|
### 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.
|
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
|
### 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
|
* Container - A summary of child prefixes
|
||||||
* Active - Provisioned and in use
|
* Active - Provisioned and in use
|
||||||
* Reserved - Earmarked for future use
|
* Reserved - Earmarked for future use
|
||||||
* Deprecated - No longer in 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
|
### Roles
|
||||||
|
|
||||||
Whereas a status describes a prefix's operational state, a role describes its function. For example, roles might include:
|
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
|
* Lab
|
||||||
* Out-of-band
|
* 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -8,6 +8,27 @@ 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):
|
class RackGroupFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
|
@ -326,10 +326,10 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
display_field='display_name',
|
display_field='display_name',
|
||||||
attrs={'filter-for': 'position'}
|
attrs={'filter-for': 'position'}
|
||||||
))
|
))
|
||||||
position = forms.TypedChoiceField(required=False, empty_value=None, widget=APISelect(
|
position = forms.TypedChoiceField(required=False, empty_value=None,
|
||||||
api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
|
help_text="For multi-U devices, this is the lowest occupied rack unit.",
|
||||||
disabled_indicator='device',
|
widget=APISelect(api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
|
||||||
))
|
disabled_indicator='device'))
|
||||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
|
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
|
||||||
widget=forms.Select(attrs={'filter-for': 'device_type'}))
|
widget=forms.Select(attrs={'filter-for': 'device_type'}))
|
||||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Model', widget=APISelect(
|
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Model', widget=APISelect(
|
||||||
@ -386,10 +386,13 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
# Rack position
|
# Rack position
|
||||||
try:
|
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')):
|
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')):
|
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:
|
else:
|
||||||
position_choices = []
|
position_choices = []
|
||||||
except Rack.DoesNotExist:
|
except Rack.DoesNotExist:
|
||||||
@ -424,7 +427,7 @@ class DeviceFromCSVForm(forms.ModelForm):
|
|||||||
'invalid_choice': 'Invalid site name.',
|
'invalid_choice': 'Invalid site name.',
|
||||||
})
|
})
|
||||||
rack_name = forms.CharField()
|
rack_name = forms.CharField()
|
||||||
face = forms.ChoiceField(choices=[('front', 'Front'), ('rear', 'Rear')])
|
face = forms.CharField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
@ -443,7 +446,7 @@ class DeviceFromCSVForm(forms.ModelForm):
|
|||||||
try:
|
try:
|
||||||
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
|
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
|
||||||
except DeviceType.DoesNotExist:
|
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
|
# Validate rack
|
||||||
if site and rack_name:
|
if site and rack_name:
|
||||||
@ -454,11 +457,15 @@ class DeviceFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean_face(self):
|
def clean_face(self):
|
||||||
face = self.cleaned_data['face']
|
face = self.cleaned_data['face']
|
||||||
if face.lower() == 'front':
|
if face:
|
||||||
return 0
|
try:
|
||||||
if face.lower() == 'rear':
|
return {
|
||||||
return 1
|
'front': 0,
|
||||||
raise forms.ValidationError("Invalid rack face ({})".format(face))
|
'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):
|
class DeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||||
@ -1036,20 +1043,29 @@ class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
connection_list = []
|
connection_list = []
|
||||||
|
occupied_interfaces = []
|
||||||
|
|
||||||
for i, record in enumerate(records, start=1):
|
for i, record in enumerate(records, start=1):
|
||||||
form = self.fields['csv'].csv_form(data=record)
|
form = self.fields['csv'].csv_form(data=record)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
|
interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
|
||||||
name=form.cleaned_data['interface_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'],
|
interface_b = Interface.objects.get(device=form.cleaned_data['device_b'],
|
||||||
name=form.cleaned_data['interface_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)
|
connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b)
|
||||||
if form.cleaned_data['status'] == 'planned':
|
if form.cleaned_data['status'] == 'planned':
|
||||||
connection.connection_status = CONNECTION_STATUS_PLANNED
|
connection.connection_status = CONNECTION_STATUS_PLANNED
|
||||||
else:
|
else:
|
||||||
connection.connection_status = CONNECTION_STATUS_CONNECTED
|
connection.connection_status = CONNECTION_STATUS_CONNECTED
|
||||||
connection_list.append(connection)
|
connection_list.append(connection)
|
||||||
|
occupied_interfaces.append(interface_a)
|
||||||
|
occupied_interfaces.append(interface_b)
|
||||||
else:
|
else:
|
||||||
for field, errors in form.errors.items():
|
for field, errors in form.errors.items():
|
||||||
for e in errors:
|
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_100M_COPPER = 800
|
||||||
IFACE_FF_1GE_COPPER = 1000
|
IFACE_FF_1GE_COPPER = 1000
|
||||||
IFACE_FF_SFP = 1100
|
IFACE_FF_SFP = 1100
|
||||||
|
IFACE_FF_10GE_COPPER = 1150
|
||||||
IFACE_FF_SFP_PLUS = 1200
|
IFACE_FF_SFP_PLUS = 1200
|
||||||
IFACE_FF_XFP = 1300
|
IFACE_FF_XFP = 1300
|
||||||
IFACE_FF_QSFP_PLUS = 1400
|
IFACE_FF_QSFP_PLUS = 1400
|
||||||
IFACE_FF_CHOICES = [
|
IFACE_FF_CHOICES = [
|
||||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||||
[IFACE_FF_100M_COPPER, '10/100M (Copper)'],
|
[IFACE_FF_100M_COPPER, '10/100M (100BASE-TX)'],
|
||||||
[IFACE_FF_1GE_COPPER, '1GE (Copper)'],
|
[IFACE_FF_1GE_COPPER, '1GE (1000BASE-T)'],
|
||||||
[IFACE_FF_SFP, '1GE (SFP)'],
|
[IFACE_FF_SFP, '1GE (SFP)'],
|
||||||
|
[IFACE_FF_10GE_COPPER, '10GE (10GBASE-T)'],
|
||||||
[IFACE_FF_SFP_PLUS, '10GE (SFP+)'],
|
[IFACE_FF_SFP_PLUS, '10GE (SFP+)'],
|
||||||
[IFACE_FF_XFP, '10GE (XFP)'],
|
[IFACE_FF_XFP, '10GE (XFP)'],
|
||||||
[IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'],
|
[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):
|
class Site(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||||
@ -187,7 +231,8 @@ class Rack(CreatedUpdatedModel):
|
|||||||
|
|
||||||
# Validate that Rack is tall enough to house the installed Devices
|
# Validate that Rack is tall enough to house the installed Devices
|
||||||
if self.pk:
|
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
|
min_height = top_device.position + top_device.device_type.u_height - 1
|
||||||
if self.u_height < min_height:
|
if self.u_height < min_height:
|
||||||
raise ValidationError("Rack must be at least {}U tall with currently installed devices."
|
raise ValidationError("Rack must be at least {}U tall with currently installed devices."
|
||||||
@ -212,12 +257,13 @@ class Rack(CreatedUpdatedModel):
|
|||||||
return "{} ({})".format(self.name, self.facility_id)
|
return "{} ({})".format(self.name, self.facility_id)
|
||||||
return self.name
|
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'}
|
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.
|
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 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
|
:param remove_redundant: If True, rack units occupied by a device already listed will be omitted
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -228,7 +274,9 @@ class Rack(CreatedUpdatedModel):
|
|||||||
# Add devices to rack units list
|
# Add devices to rack units list
|
||||||
if self.pk:
|
if self.pk:
|
||||||
for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\
|
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:
|
if remove_redundant:
|
||||||
elevation[device.position]['device'] = device
|
elevation[device.position]['device'] = device
|
||||||
for u in range(device.position + 1, device.position + device.device_type.u_height):
|
for u in range(device.position + 1, device.position + device.device_type.u_height):
|
||||||
@ -407,6 +455,13 @@ class PowerOutletTemplate(models.Model):
|
|||||||
return self.name
|
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):
|
class InterfaceTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A template for a physical data interface on a new Device.
|
A template for a physical data interface on a new Device.
|
||||||
@ -416,6 +471,8 @@ class InterfaceTemplate(models.Model):
|
|||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
||||||
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
||||||
|
|
||||||
|
objects = InterfaceTemplateManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ['device_type', 'name']
|
||||||
@ -511,7 +568,10 @@ class Device(CreatedUpdatedModel):
|
|||||||
raise ValidationError("Must specify rack face with rack position.")
|
raise ValidationError("Must specify rack face with rack position.")
|
||||||
|
|
||||||
# Validate rack space
|
# Validate rack space
|
||||||
|
try:
|
||||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
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 []
|
exclude_list = [self.pk] if self.pk else []
|
||||||
try:
|
try:
|
||||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||||
@ -707,18 +767,8 @@ class PowerOutlet(models.Model):
|
|||||||
class InterfaceManager(models.Manager):
|
class InterfaceManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
qs = super(InterfaceManager, self).get_queryset()
|
||||||
Cast up to three interface slot/position IDs as independent integers and order appropriately. This ensures that
|
return order_interfaces(qs, 'dcim_interface.name', ('device',))
|
||||||
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')
|
|
||||||
|
|
||||||
def virtual(self):
|
def virtual(self):
|
||||||
return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL)
|
return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL)
|
||||||
|
@ -5,7 +5,7 @@ from utilities.tables import BaseTable, ToggleColumn
|
|||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, InterfaceTemplate,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, InterfaceTemplate,
|
||||||
Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
|
Interface, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -305,5 +305,5 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
interface_b = tables.Column(verbose_name='Interface B')
|
interface_b = tables.Column(verbose_name='Interface B')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerPort
|
model = Interface
|
||||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
||||||
|
@ -47,7 +47,7 @@ class SiteTest(APITestCase):
|
|||||||
graph_fields = [
|
graph_fields = [
|
||||||
'name',
|
'name',
|
||||||
'embed_url',
|
'embed_url',
|
||||||
'link',
|
'embed_link',
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_get_list(self, endpoint='/api/dcim/sites/'):
|
def test_get_list(self, endpoint='/api/dcim/sites/'):
|
||||||
|
@ -61,6 +61,7 @@ def expand_pattern(string):
|
|||||||
|
|
||||||
class SiteListView(ObjectListView):
|
class SiteListView(ObjectListView):
|
||||||
queryset = Site.objects.all()
|
queryset = Site.objects.all()
|
||||||
|
filter = filters.SiteFilter
|
||||||
table = tables.SiteTable
|
table = tables.SiteTable
|
||||||
template_name = 'dcim/site_list.html'
|
template_name = 'dcim/site_list.html'
|
||||||
|
|
||||||
@ -75,11 +76,13 @@ def site(request, slug):
|
|||||||
'vlan_count': VLAN.objects.filter(site=site).count(),
|
'vlan_count': VLAN.objects.filter(site=site).count(),
|
||||||
'circuit_count': Circuit.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)
|
topology_maps = TopologyMap.objects.filter(site=site)
|
||||||
|
|
||||||
return render(request, 'dcim/site.html', {
|
return render(request, 'dcim/site.html', {
|
||||||
'site': site,
|
'site': site,
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
|
'rack_groups': rack_groups,
|
||||||
'topology_maps': topology_maps,
|
'topology_maps': topology_maps,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1514,6 +1517,9 @@ def module_add(request, pk):
|
|||||||
module.device = device
|
module.device = device
|
||||||
module.save()
|
module.save()
|
||||||
messages.success(request, "Added module {} to {}".format(module.name, module.device.name))
|
messages.success(request, "Added module {} to {}".format(module.name, module.device.name))
|
||||||
|
if '_addanother' in request.POST:
|
||||||
|
return redirect('dcim:module_add', pk=module.device.pk)
|
||||||
|
else:
|
||||||
return redirect('dcim:device_inventory', pk=module.device.pk)
|
return redirect('dcim:device_inventory', pk=module.device.pk)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import pydot
|
import graphviz
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -49,32 +49,30 @@ class TopologyMapView(APIView):
|
|||||||
tmap = get_object_or_404(TopologyMap, slug=slug)
|
tmap = get_object_or_404(TopologyMap, slug=slug)
|
||||||
|
|
||||||
# Construct the graph
|
# 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):
|
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
|
# 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:
|
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
|
# Add each device to the graph
|
||||||
devices = []
|
devices = []
|
||||||
for query in device_set.split(','):
|
for query in device_set.split(','):
|
||||||
devices += Device.objects.filter(name__regex=query)
|
devices += Device.objects.filter(name__regex=query)
|
||||||
for d in devices:
|
for d in devices:
|
||||||
node = pydot.Node(d.name)
|
subgraph.node(d.name)
|
||||||
subgraph.add_node(node)
|
|
||||||
|
|
||||||
# Add an invisible connection to each successive device in a set to enforce horizontal order
|
# Add an invisible connection to each successive device in a set to enforce horizontal order
|
||||||
for j in range(0, len(devices) - 1):
|
for j in range(0, len(devices) - 1):
|
||||||
edge = pydot.Edge(devices[j].name, devices[j + 1].name)
|
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
|
||||||
# edge.set('style', 'invis') doesn't seem to work for some reason
|
|
||||||
edge.set_style('invis')
|
|
||||||
subgraph.add_edge(edge)
|
|
||||||
|
|
||||||
graph.add_subgraph(subgraph)
|
graph.subgraph(subgraph)
|
||||||
|
|
||||||
# Compile list of all devices
|
# Compile list of all devices
|
||||||
device_superset = Q()
|
device_superset = Q()
|
||||||
@ -87,17 +85,14 @@ class TopologyMapView(APIView):
|
|||||||
connections = InterfaceConnection.objects.filter(interface_a__device__in=devices,
|
connections = InterfaceConnection.objects.filter(interface_a__device__in=devices,
|
||||||
interface_b__device__in=devices)
|
interface_b__device__in=devices)
|
||||||
for c in connections:
|
for c in connections:
|
||||||
edge = pydot.Edge(c.interface_a.device.name, c.interface_b.device.name)
|
graph.edge(c.interface_a.device.name, c.interface_b.device.name)
|
||||||
graph.add_edge(edge)
|
|
||||||
|
|
||||||
# Write the image to disk and return
|
# Get the image data and return
|
||||||
topo_file = tempfile.NamedTemporaryFile()
|
|
||||||
try:
|
try:
|
||||||
graph.write(topo_file.name, format='png')
|
topo_data = graph.pipe(format='png')
|
||||||
except:
|
except:
|
||||||
return HttpResponse("There was an error generating the requested graph. Ensure that the GraphViz "
|
return HttpResponse("There was an error generating the requested graph. Ensure that the GraphViz "
|
||||||
"executables have been installed correctly.")
|
"executables have been installed correctly.")
|
||||||
response = HttpResponse(FileWrapper(topo_file), content_type='image/png')
|
response = HttpResponse(topo_data, content_type='image/png')
|
||||||
topo_file.close()
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
|
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
|
from . import serializers
|
||||||
|
|
||||||
@ -12,6 +12,7 @@ class VRFListView(generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
queryset = VRF.objects.all()
|
queryset = VRF.objects.all()
|
||||||
serializer_class = serializers.VRFSerializer
|
serializer_class = serializers.VRFSerializer
|
||||||
|
filter_class = VRFFilter
|
||||||
|
|
||||||
|
|
||||||
class VRFDetailView(generics.RetrieveAPIView):
|
class VRFDetailView(generics.RetrieveAPIView):
|
||||||
|
@ -10,7 +10,13 @@ from .lookups import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prefix_validator(prefix):
|
||||||
|
if prefix.ip != prefix.cidr.ip:
|
||||||
|
raise ValidationError("{} is not a valid prefix. Did you mean {}?".format(prefix, prefix.cidr))
|
||||||
|
|
||||||
|
|
||||||
class BaseIPField(models.Field):
|
class BaseIPField(models.Field):
|
||||||
|
default_validators = [prefix_validator]
|
||||||
|
|
||||||
def python_type(self):
|
def python_type(self):
|
||||||
return IPNetwork
|
return IPNetwork
|
||||||
|
@ -46,9 +46,14 @@ class PrefixFilter(django_filters.FilterSet):
|
|||||||
action='search_by_parent',
|
action='search_by_parent',
|
||||||
label='Parent prefix',
|
label='Parent prefix',
|
||||||
)
|
)
|
||||||
|
vrf = django_filters.MethodFilter(
|
||||||
|
action='_vrf',
|
||||||
|
label='VRF',
|
||||||
|
)
|
||||||
|
# Duplicate of `vrf` for backward-compatibility
|
||||||
vrf_id = django_filters.MethodFilter(
|
vrf_id = django_filters.MethodFilter(
|
||||||
action='vrf',
|
action='_vrf',
|
||||||
label='VRF (ID)',
|
label='VRF',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
@ -84,7 +89,7 @@ class PrefixFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
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):
|
def search(self, queryset, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
@ -104,7 +109,7 @@ class PrefixFilter(django_filters.FilterSet):
|
|||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
def vrf(self, queryset, value):
|
def _vrf(self, queryset, value):
|
||||||
if str(value) == '':
|
if str(value) == '':
|
||||||
return queryset
|
return queryset
|
||||||
try:
|
try:
|
||||||
@ -121,10 +126,14 @@ class IPAddressFilter(django_filters.FilterSet):
|
|||||||
action='search',
|
action='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
vrf = django_filters.MethodFilter(
|
||||||
name='vrf',
|
action='_vrf',
|
||||||
queryset=VRF.objects.all(),
|
label='VRF',
|
||||||
label='VRF (ID)',
|
)
|
||||||
|
# Duplicate of `vrf` for backward-compatibility
|
||||||
|
vrf_id = django_filters.MethodFilter(
|
||||||
|
action='_vrf',
|
||||||
|
label='VRF',
|
||||||
)
|
)
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='interface__device',
|
name='interface__device',
|
||||||
@ -155,6 +164,17 @@ class IPAddressFilter(django_filters.FilterSet):
|
|||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
return queryset.none()
|
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):
|
class VLANFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
@ -13,6 +13,10 @@ from .models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
|
||||||
|
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
@ -215,6 +219,7 @@ class PrefixBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
|
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")
|
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')
|
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)
|
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||||
description = forms.CharField(max_length=50, required=False)
|
description = forms.CharField(max_length=50, required=False)
|
||||||
|
|
||||||
@ -444,6 +449,7 @@ class VLANImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
class VLANBulkEditForm(forms.Form, BootstrapMixin):
|
class VLANBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
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)
|
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,6 +121,12 @@ class Aggregate(CreatedUpdatedModel):
|
|||||||
raise ValidationError("{} is already covered by an existing aggregate ({})"
|
raise ValidationError("{} is already covered by an existing aggregate ({})"
|
||||||
.format(self.prefix, covering_aggregates[0]))
|
.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):
|
def save(self, *args, **kwargs):
|
||||||
if self.prefix:
|
if self.prefix:
|
||||||
# Infer address family from IPNetwork object
|
# 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)
|
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))
|
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)\
|
parent_prefixes_table = tables.PrefixBriefTable(parent_prefixes)
|
||||||
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
|
||||||
|
|
||||||
|
# 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)
|
related_ips_table = tables.IPAddressBriefTable(related_ips)
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(related_ips_table)
|
|
||||||
|
|
||||||
return render(request, 'ipam/ipaddress.html', {
|
return render(request, 'ipam/ipaddress.html', {
|
||||||
'ipaddress': ipaddress,
|
'ipaddress': ipaddress,
|
||||||
'parent_prefixes': parent_prefixes,
|
'parent_prefixes_table': parent_prefixes_table,
|
||||||
|
'duplicate_ips_table': duplicate_ips_table,
|
||||||
'related_ips_table': related_ips_table,
|
'related_ips_table': related_ips_table,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
75
netbox/netbox/configuration.docker.py
Normal file
75
netbox/netbox/configuration.docker.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import os
|
||||||
|
#########################
|
||||||
|
# #
|
||||||
|
# Required settings #
|
||||||
|
# #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
|
||||||
|
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
||||||
|
#
|
||||||
|
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
||||||
|
ALLOWED_HOSTS = [os.environ.get('ALLOWED_HOSTS', '')]
|
||||||
|
|
||||||
|
# PostgreSQL database configuration.
|
||||||
|
DATABASE = {
|
||||||
|
'NAME': os.environ.get('DB_NAME', 'netbox'), # Database name
|
||||||
|
'USER': os.environ.get('DB_USER', ''), # PostgreSQL username
|
||||||
|
'PASSWORD': os.environ.get('DB_PASSWORD', ''), # PostgreSQL password
|
||||||
|
'HOST': os.environ.get('DB_HOST', 'localhost'), # Database server
|
||||||
|
'PORT': os.environ.get('DB_PORT', ''), # Database port (leave blank for default)
|
||||||
|
}
|
||||||
|
|
||||||
|
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
|
||||||
|
# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
|
||||||
|
# symbols. NetBox will not run without this defined. For more information, see
|
||||||
|
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY
|
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY', '')
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# #
|
||||||
|
# Optional settings #
|
||||||
|
# #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
# Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
|
||||||
|
# application errors (assuming correct email settings are provided).
|
||||||
|
ADMINS = [
|
||||||
|
# ['John Doe', 'jdoe@example.com'],
|
||||||
|
]
|
||||||
|
|
||||||
|
# Email settings
|
||||||
|
EMAIL = {
|
||||||
|
'SERVER': os.environ.get('EMAIL_SERVER', 'localhost'),
|
||||||
|
'PORT': os.environ.get('EMAIL_PORT', 25),
|
||||||
|
'USERNAME': os.environ.get('EMAIL_USERNAME', ''),
|
||||||
|
'PASSWORD': os.environ.get('EMAIL_PASSWORD', ''),
|
||||||
|
'TIMEOUT': os.environ.get('EMAIL_TIMEOUT', 10), # seconds
|
||||||
|
'FROM_EMAIL': os.environ.get('EMAIL_FROM', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
|
||||||
|
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||||
|
LOGIN_REQUIRED = os.environ.get('LOGIN_REQUIRED', False)
|
||||||
|
|
||||||
|
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||||
|
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
|
||||||
|
|
||||||
|
# Credentials that NetBox will use to access live devices.
|
||||||
|
NETBOX_USERNAME = os.environ.get('NETBOX_USERNAME', '')
|
||||||
|
NETBOX_PASSWORD = os.environ.get('NETBOX_PASSWORD', '')
|
||||||
|
|
||||||
|
# Determine how many objects to display per page within a list. (Default: 50)
|
||||||
|
PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50)
|
||||||
|
|
||||||
|
# Time zone (default: UTC)
|
||||||
|
TIME_ZONE = os.environ.get('TIME_ZONE', 'UTC')
|
||||||
|
|
||||||
|
# Date/time formatting. See the following link for supported formats:
|
||||||
|
# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
|
||||||
|
DATE_FORMAT = os.environ.get('DATE_FORMAT', 'N j, Y')
|
||||||
|
SHORT_DATE_FORMAT = os.environ.get('SHORT_DATE_FORMAT', 'Y-m-d')
|
||||||
|
TIME_FORMAT = os.environ.get('TIME_FORMAT', 'g:i a')
|
||||||
|
SHORT_TIME_FORMAT = os.environ.get('SHORT_TIME_FORMAT', 'H:i:s')
|
||||||
|
DATETIME_FORMAT = os.environ.get('DATETIME_FORMAT', 'N j, Y g:i a')
|
||||||
|
SHORT_DATETIME_FORMAT = os.environ.get('SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
@ -11,6 +11,8 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = '1.0.8-dev'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
try:
|
try:
|
||||||
|
@ -9,7 +9,8 @@ body {
|
|||||||
padding-top: 70px;
|
padding-top: 70px;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 1340px;
|
width: auto;
|
||||||
|
max-width: 1340px;
|
||||||
}
|
}
|
||||||
.wrapper {
|
.wrapper {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
@ -7,9 +7,9 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// Slugify
|
// Slugify
|
||||||
function slugify(s, num_chars) {
|
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+|\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
|
s = s.toLowerCase(); // Convert to lowercase
|
||||||
return s.substring(0, num_chars); // Trim to first num_chars chars
|
return s.substring(0, num_chars); // Trim to first num_chars chars
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p>There was a problem with your request. This error has been logged and administrative staff have
|
<p>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.</p>
|
been notified. Please return to the home page and try again.</p>
|
||||||
|
<p>If you are responsible for this installation, please consider
|
||||||
|
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>.</p>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a href="/" class="btn btn-primary">Home Page</a>
|
<a href="/" class="btn btn-primary">Home Page</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,9 +13,16 @@
|
|||||||
<nav class="navbar navbar-default navbar-fixed-top">
|
<nav class="navbar navbar-default navbar-fixed-top">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
<a class="navbar-brand" href="/">NetBox</a>
|
<a class="navbar-brand" href="/">NetBox</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="navbar" class="navbar-collapse collapse">
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
|
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' %} active{% endif %}">
|
||||||
{% if perms.dcim.add_site %}
|
{% if perms.dcim.add_site %}
|
||||||
@ -201,11 +208,12 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<li><a href="{% url 'admin:index' %}"><i class="glyphicon glyphicon-cog" aria-hidden="true"></i> Admin</a></li>
|
<li><a href="{% url 'admin:index' %}"><i class="glyphicon glyphicon-cog" aria-hidden="true"></i> Admin</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<li><a href="{% url 'users:profile' %}"><i class="glyphicon glyphicon-user" aria-hidden="true"></i> Profile</a></li>
|
<li><a href="{% url 'users:profile' %}"><i class="glyphicon glyphicon-user" aria-hidden="true"></i> Profile</a></li>
|
||||||
<li><a href="{% url 'logout' %}"><i class="glyphicon glyphicon-log-out" aria-hidden="true"></i> Log out</a></li>
|
<li><a href="{% url 'logout' %}"><i class="glyphicon glyphicon-log-out" aria-hidden="true"></i> Log out</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -237,7 +245,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<p class="text-muted">{{ settings.HOSTNAME }}</p>
|
<p class="text-muted">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<p class="text-muted">{% now 'Y-m-d H:i:s T' %}</p>
|
<p class="text-muted">{% now 'Y-m-d H:i:s T' %}</p>
|
||||||
|
@ -68,18 +68,18 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Position (U)</td>
|
<td>Position (U)</td>
|
||||||
<td>Numeric rack position (optional)</td>
|
<td>Lowest rack unit occupied by the device (optional)</td>
|
||||||
<td>21</td>
|
<td>21</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Face</td>
|
<td>Face</td>
|
||||||
<td>Rack face; front or rear (optional)</td>
|
<td>Rack face; front or rear (optional)</td>
|
||||||
<td>rear</td>
|
<td>Rear</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<h4>Example</h4>
|
||||||
<pre>rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,rear</pre>
|
<pre>rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -8,6 +8,14 @@
|
|||||||
<h1>Interface Connections Import</h1>
|
<h1>Interface Connections Import</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<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">
|
<form action="." method="post" class="form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
|
@ -6,6 +6,26 @@
|
|||||||
{% block title %}{{ site }}{% endblock %}
|
{% block title %}{{ site }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
|
||||||
|
<li>{{ site }}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<form action="{% url 'dcim:site_list' %}" method="get">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="Search" />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site_graphs' pk=site.pk %}" title="Show graphs">
|
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site_graphs' pk=site.pk %}" title="Show graphs">
|
||||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
||||||
@ -124,6 +144,25 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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 panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Topology Maps</strong>
|
<strong>Topology Maps</strong>
|
||||||
@ -132,7 +171,7 @@
|
|||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for tm in topology_maps %}
|
{% for tm in topology_maps %}
|
||||||
<tr>
|
<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>
|
<td>{{ tm.description }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -14,5 +14,28 @@
|
|||||||
{% include 'inc/export_button.html' with obj_type='sites' %}
|
{% include 'inc/export_button.html' with obj_type='sites' %}
|
||||||
</div>
|
</div>
|
||||||
<h1>Sites</h1>
|
<h1>Sites</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
{% render_table table 'table.html' %}
|
{% render_table table 'table.html' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Search</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form action="{% url 'dcim:site_list' %}" method="get">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -119,31 +119,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
{% with heading='Parent Prefixes' %}
|
||||||
<div class="panel-heading">
|
{% render_table parent_prefixes_table 'panel_table.html' %}
|
||||||
<strong>Parent Prefixes</strong>
|
{% endwith %}
|
||||||
</div>
|
{% if duplicate_ips_table.rows %}
|
||||||
{% if parent_prefixes %}
|
{% with heading='Duplicate IP Addresses' panel_class='danger' %}
|
||||||
<table class="table table-hover panel-body">
|
{% render_table duplicate_ips_table 'panel_table.html' %}
|
||||||
{% for p in parent_prefixes %}
|
{% endwith %}
|
||||||
<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 %}
|
{% 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='Related IP Addresses' %}
|
{% with heading='Related IP Addresses' %}
|
||||||
{% render_table related_ips_table 'panel_table.html' %}
|
{% render_table related_ips_table 'panel_table.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -253,6 +253,9 @@ class BulkImportForm(forms.Form):
|
|||||||
else:
|
else:
|
||||||
for field, errors in obj_form.errors.items():
|
for field, errors in obj_form.errors.items():
|
||||||
for e in errors:
|
for e in errors:
|
||||||
|
if field == '__all__':
|
||||||
|
self.add_error('csv', "Record {}: {}".format(i, e))
|
||||||
|
else:
|
||||||
self.add_error('csv', "Record {} ({}): {}".format(i, field, e))
|
self.add_error('csv', "Record {} ({}): {}".format(i, field, e))
|
||||||
|
|
||||||
self.cleaned_data['csv'] = obj_list
|
self.cleaned_data['csv'] = obj_list
|
||||||
|
@ -120,7 +120,7 @@ class ObjectEditView(View):
|
|||||||
'obj': obj,
|
'obj': obj,
|
||||||
'obj_type': self.model._meta.verbose_name,
|
'obj_type': self.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse(self.cancel_url) if self.cancel_url else obj.get_absolute_url(),
|
'cancel_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else reverse(self.cancel_url),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@ -137,9 +137,9 @@ class ObjectEditView(View):
|
|||||||
msg = 'Created ' if obj_created else 'Modified '
|
msg = 'Created ' if obj_created else 'Modified '
|
||||||
msg += self.model._meta.verbose_name
|
msg += self.model._meta.verbose_name
|
||||||
if hasattr(obj, 'get_absolute_url'):
|
if hasattr(obj, 'get_absolute_url'):
|
||||||
msg += ' <a href="{}">{}</a>'.format(obj.get_absolute_url(), obj)
|
msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), obj)
|
||||||
else:
|
else:
|
||||||
msg += ' {}'.format(obj)
|
msg = '{} {}'.format(msg, obj)
|
||||||
messages.success(request, msg)
|
messages.success(request, msg)
|
||||||
if obj_created:
|
if obj_created:
|
||||||
UserAction.objects.log_create(request.user, obj, msg)
|
UserAction.objects.log_create(request.user, obj, msg)
|
||||||
@ -157,7 +157,7 @@ class ObjectEditView(View):
|
|||||||
'obj': obj,
|
'obj': obj,
|
||||||
'obj_type': self.model._meta.verbose_name,
|
'obj_type': self.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse(self.cancel_url) if self.cancel_url else obj.get_absolute_url(),
|
'cancel_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else reverse(self.cancel_url),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ django-filter==0.13.0
|
|||||||
django-rest-swagger==0.3.7
|
django-rest-swagger==0.3.7
|
||||||
django-tables2==1.2.1
|
django-tables2==1.2.1
|
||||||
djangorestframework==3.3.3
|
djangorestframework==3.3.3
|
||||||
|
graphviz==0.4.10
|
||||||
Markdown==2.6.6
|
Markdown==2.6.6
|
||||||
ncclient==0.4.7
|
ncclient==0.4.7
|
||||||
netaddr==0.7.18
|
netaddr==0.7.18
|
||||||
@ -12,6 +13,5 @@ paramiko==2.0.0
|
|||||||
psycopg2==2.6.1
|
psycopg2==2.6.1
|
||||||
py-gfm==0.1.3
|
py-gfm==0.1.3
|
||||||
pycrypto==2.6.1
|
pycrypto==2.6.1
|
||||||
pydot==1.0.2
|
|
||||||
sqlparse==0.1.19
|
sqlparse==0.1.19
|
||||||
xmltodict==0.10.2
|
xmltodict==0.10.2
|
||||||
|
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
|
27
upgrade.sh
Executable file
27
upgrade.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 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).
|
||||||
|
|
||||||
|
# Optionally use sudo if not already root, and always prompt for password
|
||||||
|
# before running the command
|
||||||
|
PREFIX="sudo -k "
|
||||||
|
if [ "$(whoami)" = "root" ]; then
|
||||||
|
# When running upgrade as root, ask user to confirm if they wish to
|
||||||
|
# continue
|
||||||
|
read -n1 -rsp $'Running NetBox upgrade as root, press any key to continue or ^C to cancel\n'
|
||||||
|
PREFIX=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install any new Python packages
|
||||||
|
COMMAND="${PREFIX}pip install -r requirements.txt --upgrade"
|
||||||
|
echo "Updating required Python packages ($COMMAND)..."
|
||||||
|
eval $COMMAND
|
||||||
|
|
||||||
|
# Apply any database migrations
|
||||||
|
./netbox/manage.py migrate
|
||||||
|
|
||||||
|
# Collect static files
|
||||||
|
./netbox/manage.py collectstatic --noinput
|
Loading…
Reference in New Issue
Block a user