Merge pull request #726 from digitalocean/develop

Release v1.7.2
This commit is contained in:
Jeremy Stretch 2016-12-06 14:55:19 -05:00 committed by GitHub
commit 66be85a41f
40 changed files with 360 additions and 213 deletions

View File

@ -43,8 +43,9 @@ take some time for someone to address your issue.
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're * First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
requesting is already listed. (Be sure to search closed issues as well, since some feature requests are rejected.) If requesting is already listed. (Be sure to search closed issues as well, since some feature requests are rejected.) If
the feature you'd like to see has already been requested, click "add a reaction" in the top right corner of the issue the feature you'd like to see has already been requested, click "add a reaction" in the top right corner of the issue
and add a thumbs up (+1). This ensures that the issue has a better chance of making it onto the roadmap. Also feel free and add a thumbs up. This ensures that the issue has a better chance of making it onto the roadmap. Also feel free
to add a comment with any additional justification for the feature. to add a comment with any additional justification for the feature. (However, note that comments with no substance
other than a "+1" will be deleted as spam. Please use GitHub's reactions feature to indicate your support.)
* While suggestions for new features are welcome, it's important to limit the scope of NetBox's feature set to avoid * While suggestions for new features are welcome, it's important to limit the scope of NetBox's feature set to avoid
feature creep. For example, the following features would be firmly out of scope for NetBox: feature creep. For example, the following features would be firmly out of scope for NetBox:

View File

@ -4,10 +4,10 @@ This guide demonstrates how to build and run NetBox as a Docker container. It as
To get NetBox up and running: To get NetBox up and running:
``` ```no-highlight
git clone -b master https://github.com/digitalocean/netbox.git # git clone -b master https://github.com/digitalocean/netbox.git
cd netbox # cd netbox
docker-compose up -d # docker-compose up -d
``` ```
The application will be available on http://localhost/ after a few minutes. The application will be available on http://localhost/ after a few minutes.

View File

@ -7,19 +7,19 @@ built-in Django users in the event of a failure.
On Ubuntu: On Ubuntu:
``` ```no-highlight
sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev
``` ```
On CentOS: On CentOS:
``` ```no-highlight
sudo yum install -y python-devel openldap-devel sudo yum install -y python-devel openldap-devel
``` ```
## Install django-auth-ldap ## Install django-auth-ldap
``` ```no-highlight
sudo pip install django-auth-ldap sudo pip install django-auth-ldap
``` ```

View File

@ -2,13 +2,13 @@
**Debian/Ubuntu** **Debian/Ubuntu**
``` ```no-highlight
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev # apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
``` ```
**CentOS/RHEL** **CentOS/RHEL**
``` ```no-highlight
# yum install -y epel-release # yum install -y epel-release
# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel # yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
``` ```
@ -19,7 +19,7 @@ You may opt to install NetBox either from a numbered release or by cloning the m
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox`. Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox`.
``` ```no-highlight
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz # wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt # tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/ # cd /opt/
@ -31,28 +31,27 @@ Download the [latest stable release](https://github.com/digitalocean/netbox/rele
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`.
``` ```no-highlight
# mkdir -p /opt/netbox/ # mkdir -p /opt/netbox/ && cd /opt/netbox/
# cd /opt/netbox/
``` ```
If `git` is not already installed, install it: If `git` is not already installed, install it:
**Debian/Ubuntu** **Debian/Ubuntu**
``` ```no-highlight
# apt-get install -y git # apt-get install -y git
``` ```
**CentOS/RHEL** **CentOS/RHEL**
``` ```no-highlight
# yum install -y git # yum install -y git
``` ```
Next, clone the **master** branch of the NetBox GitHub repository into the current directory: Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
``` ```no-highlight
# git clone -b master https://github.com/digitalocean/netbox.git . # git clone -b master https://github.com/digitalocean/netbox.git .
Cloning into '.'... Cloning into '.'...
remote: Counting objects: 1994, done. remote: Counting objects: 1994, done.
@ -67,7 +66,7 @@ Checking connectivity... done.
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.) Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
``` ```no-highlight
# pip install -r requirements.txt # pip install -r requirements.txt
``` ```
@ -75,7 +74,7 @@ Install the required Python packages using pip. (If you encounter any compilatio
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`. Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
``` ```no-highlight
# cd netbox/netbox/ # cd netbox/netbox/
# cp configuration.example.py configuration.py # cp configuration.example.py configuration.py
``` ```
@ -92,7 +91,7 @@ This is a list of the valid hostnames by which this server can be reached. You m
Example: Example:
``` ```python
ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123'] ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123']
``` ```
@ -102,7 +101,7 @@ This parameter holds the database configuration details. You must define the use
Example: Example:
``` ```python
DATABASE = { DATABASE = {
'NAME': 'netbox', # Database name 'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username 'USER': 'netbox', # PostgreSQL username
@ -125,7 +124,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):
``` ```no-highlight
# cd /opt/netbox/netbox/ # cd /opt/netbox/netbox/
# ./manage.py migrate # ./manage.py migrate
Operations to perform: Operations to perform:
@ -144,7 +143,7 @@ If this step results in a PostgreSQL authentication error, ensure that the usern
NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox: NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox:
``` ```no-highlight
# ./manage.py createsuperuser # ./manage.py createsuperuser
Username: admin Username: admin
Email address: admin@example.com Email address: admin@example.com
@ -155,7 +154,7 @@ Superuser created successfully.
# Collect Static Files # Collect Static Files
``` ```no-highlight
# ./manage.py collectstatic # ./manage.py collectstatic
You have requested to collect static files at the destination You have requested to collect static files at the destination
@ -176,7 +175,7 @@ NetBox ships with some initial data to help you get started: RIR definitions, co
!!! note !!! note
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch. This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
``` ```no-highlight
# ./manage.py loaddata initial_data # ./manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s) Installed 43 object(s) from 4 fixture(s)
``` ```
@ -185,7 +184,7 @@ Installed 43 object(s) from 4 fixture(s)
At this point, NetBox should be able to run. We can verify this by starting a development instance: At this point, NetBox should be able to run. We can verify this by starting a development instance:
``` ```no-highlight
# ./manage.py runserver 0.0.0.0:8000 --insecure # ./manage.py runserver 0.0.0.0:8000 --insecure
Performing system checks... Performing system checks...

View File

@ -4,27 +4,27 @@ NetBox requires a PostgreSQL database to store data. MySQL is not supported, as
**Debian/Ubuntu** **Debian/Ubuntu**
``` ```no-highlight
# apt-get install -y postgresql libpq-dev python-psycopg2 # apt-get install -y postgresql libpq-dev python-psycopg2
``` ```
**CentOS/RHEL** **CentOS/RHEL**
``` ```no-highlight
# yum install -y postgresql postgresql-server postgresql-devel python-psycopg2 # yum install -y postgresql postgresql-server postgresql-devel python-psycopg2
# postgresql-setup initdb # postgresql-setup initdb
``` ```
If using CentOS, modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/data/pg_hba.conf`. For example: If using CentOS, modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/data/pg_hba.conf`. For example:
``` ```no-highlight
host all all 127.0.0.1/32 md5 host all all 127.0.0.1/32 md5
host all all ::1/128 md5 host all all ::1/128 md5
``` ```
Then, start the service: Then, start the service:
``` ```no-highlight
# systemctl start postgresql # systemctl start postgresql
``` ```
@ -35,7 +35,7 @@ At a minimum, we need to create a database for NetBox and assign it a username a
!!! danger !!! danger
DO NOT USE THE PASSWORD FROM THE EXAMPLE. DO NOT USE THE PASSWORD FROM THE EXAMPLE.
``` ```no-highlight
# sudo -u postgres psql # sudo -u postgres psql
psql (9.3.13) psql (9.3.13)
Type "help" for help. Type "help" for help.
@ -51,7 +51,7 @@ postgres=# \q
You can verify that authentication works issuing the following command and providing the configured password: You can verify that authentication works issuing the following command and providing the configured password:
``` ```no-highlight
# psql -U netbox -h localhost -W # psql -U netbox -h localhost -W
``` ```

View File

@ -8,7 +8,7 @@ Download the [latest stable release](https://github.com/digitalocean/netbox/rele
Download and extract the latest version: Download and extract the latest version:
``` ```no-highlight
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz # wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt # tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/ # cd /opt/
@ -17,13 +17,13 @@ Download and extract the latest version:
Copy the 'configuration.py' you created when first installing to the new version: Copy the 'configuration.py' you created when first installing to the new version:
``` ```no-highlight
# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py # cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py
``` ```
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well: If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
``` ```no-highlight
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py # cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
``` ```
@ -31,7 +31,7 @@ If you followed the original installation guide to set up gunicorn, be sure to c
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch: This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
``` ```no-highlight
# cd /opt/netbox # cd /opt/netbox
# git checkout master # git checkout master
# git pull origin master # git pull origin master
@ -42,7 +42,7 @@ This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most
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). 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).
``` ```no-highlight
# ./upgrade.sh # ./upgrade.sh
``` ```
@ -56,6 +56,6 @@ This script:
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`: Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
``` ```no-highlight
# sudo supervisorctl restart netbox # sudo supervisorctl restart netbox
``` ```

View File

@ -5,7 +5,7 @@ We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for
!!! info !!! info
Only Debian/Ubuntu instructions are provided here, but the installation process for CentOS/RHEL does not differ much. Please consult the documentation for those distributions for details. Only Debian/Ubuntu instructions are provided here, but the installation process for CentOS/RHEL does not differ much. Please consult the documentation for those distributions for details.
``` ```no-highlight
# apt-get install -y gunicorn supervisor # apt-get install -y gunicorn supervisor
``` ```
@ -13,13 +13,13 @@ We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for
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.
``` ```no-highlight
# apt-get install -y nginx # apt-get install -y nginx
``` ```
Once nginx is installed, save the following configuration to `/etc/nginx/sites-available/netbox`. Be sure to replace `netbox.example.com` with the domain name or IP address of your installation. (This should match the value configured for `ALLOWED_HOSTS` in `configuration.py`.) Once nginx is installed, save the following configuration to `/etc/nginx/sites-available/netbox`. Be sure to replace `netbox.example.com` with the domain name or IP address of your installation. (This should match the value configured for `ALLOWED_HOSTS` in `configuration.py`.)
``` ```nginx
server { server {
listen 80; listen 80;
@ -43,7 +43,7 @@ server {
Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created. Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created.
``` ```no-highlight
# cd /etc/nginx/sites-enabled/ # cd /etc/nginx/sites-enabled/
# rm default # rm default
# ln -s /etc/nginx/sites-available/netbox # ln -s /etc/nginx/sites-available/netbox
@ -51,7 +51,7 @@ Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sit
Restart the nginx service to use the new configuration. Restart the nginx service to use the new configuration.
``` ```no-highlight
# service nginx restart # service nginx restart
``` ```
@ -59,13 +59,13 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
## Option B: Apache ## Option B: Apache
``` ```no-highlight
# apt-get install -y apache2 # apt-get install -y apache2
``` ```
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately): Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
``` ```apache
<VirtualHost *:80> <VirtualHost *:80>
ProxyPreserveHost On ProxyPreserveHost On
@ -90,7 +90,7 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf`, enable the `proxy` and `proxy_http` modules, and reload Apache: Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf`, enable the `proxy` and `proxy_http` modules, and reload Apache:
``` ```no-highlight
# a2enmod proxy # a2enmod proxy
# a2enmod proxy_http # a2enmod proxy_http
# a2ensite netbox # a2ensite netbox
@ -103,7 +103,7 @@ To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https
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. If using CentOS/RHEL change the username from `www-data` to `nginx` or `apache`. 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. If using CentOS/RHEL change the username from `www-data` to `nginx` or `apache`.
``` ```no-highlight
command = '/usr/bin/gunicorn' command = '/usr/bin/gunicorn'
pythonpath = '/opt/netbox/netbox' pythonpath = '/opt/netbox/netbox'
bind = '127.0.0.1:8001' bind = '127.0.0.1:8001'
@ -115,7 +115,7 @@ user = 'www-data'
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed.
``` ```no-highlight
[program:netbox] [program:netbox]
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
directory = /opt/netbox/netbox/ directory = /opt/netbox/netbox/
@ -124,7 +124,7 @@ user = www-data
Then, restart the supervisor service to detect and run the gunicorn service: Then, restart the supervisor service to detect and run the gunicorn service:
``` ```no-highlight
# service supervisor restart # service supervisor restart
``` ```

View File

@ -54,7 +54,7 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
portal_url = forms.URLField(required=False, label='Portal') portal_url = forms.URLField(required=False, label='Portal')
noc_contact = forms.CharField(required=False, widget=SmallTextarea, label='NOC contact') noc_contact = forms.CharField(required=False, widget=SmallTextarea, label='NOC contact')
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact') admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
comments = CommentField() comments = CommentField(widget=SmallTextarea)
class Meta: class Meta:
nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
@ -183,7 +183,7 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)') port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)') commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
comments = CommentField() comments = CommentField(widget=SmallTextarea)
class Meta: class Meta:
nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments'] nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments']

View File

@ -1,4 +1,5 @@
import django_filters import django_filters
from netaddr.core import AddrFormatError
from django.db.models import Q from django.db.models import Q
@ -146,6 +147,10 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
action='search', action='search',
label='Search', label='Search',
) )
mac_address = django_filters.MethodFilter(
action='_mac_address',
label='MAC address',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='rack__site', name='rack__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@ -254,6 +259,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(comments__icontains=value) Q(comments__icontains=value)
).distinct() ).distinct()
def _mac_address(self, queryset, value):
try:
return queryset.filter(interfaces__mac_address=value.strip()).distinct()
except AddrFormatError:
return queryset.none()
class ConsolePortFilter(django_filters.FilterSet): class ConsolePortFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(

View File

@ -5,7 +5,7 @@
"fields": { "fields": {
"name": "Console Server", "name": "Console Server",
"slug": "console-server", "slug": "console-server",
"color": "teal" "color": "009688"
} }
}, },
{ {
@ -14,7 +14,7 @@
"fields": { "fields": {
"name": "Core Switch", "name": "Core Switch",
"slug": "core-switch", "slug": "core-switch",
"color": "blue" "color": "2196f3"
} }
}, },
{ {
@ -23,7 +23,7 @@
"fields": { "fields": {
"name": "Distribution Switch", "name": "Distribution Switch",
"slug": "distribution-switch", "slug": "distribution-switch",
"color": "blue" "color": "2196f3"
} }
}, },
{ {
@ -32,7 +32,7 @@
"fields": { "fields": {
"name": "Access Switch", "name": "Access Switch",
"slug": "access-switch", "slug": "access-switch",
"color": "blue" "color": "2196f3"
} }
}, },
{ {
@ -41,7 +41,7 @@
"fields": { "fields": {
"name": "Management Switch", "name": "Management Switch",
"slug": "management-switch", "slug": "management-switch",
"color": "orange" "color": "ff9800"
} }
}, },
{ {
@ -50,7 +50,7 @@
"fields": { "fields": {
"name": "Firewall", "name": "Firewall",
"slug": "firewall", "slug": "firewall",
"color": "red" "color": "f44336"
} }
}, },
{ {
@ -59,7 +59,7 @@
"fields": { "fields": {
"name": "Router", "name": "Router",
"slug": "router", "slug": "router",
"color": "purple" "color": "9c27b0"
} }
}, },
{ {
@ -68,7 +68,7 @@
"fields": { "fields": {
"name": "Server", "name": "Server",
"slug": "server", "slug": "server",
"color": "medium_gray" "color": "9e9e9e"
} }
}, },
{ {
@ -77,7 +77,7 @@
"fields": { "fields": {
"name": "PDU", "name": "PDU",
"slug": "pdu", "slug": "pdu",
"color": "dark_gray" "color": "607d8b"
} }
}, },
{ {

View File

@ -221,7 +221,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type') type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width') width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
u_height = forms.IntegerField(required=False, label='Height (U)') u_height = forms.IntegerField(required=False, label='Height (U)')
comments = CommentField() comments = CommentField(widget=SmallTextarea)
class Meta: class Meta:
nullable_fields = ['group', 'tenant', 'role', 'comments'] nullable_fields = ['group', 'tenant', 'role', 'comments']
@ -612,6 +612,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')), platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')),
to_field_name='slug', null_option=(0, 'None')) to_field_name='slug', null_option=(0, 'None'))
status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES)) status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
mac_address = forms.CharField(label='MAC address')
# #

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-12-06 16:35
from __future__ import unicode_literals
from django.db import migrations
import utilities.fields
COLOR_CONVERSION = {
'teal': '009688',
'green': '4caf50',
'blue': '2196f3',
'purple': '9c27b0',
'yellow': 'ffeb3b',
'orange': 'ff9800',
'red': 'f44336',
'light_gray': 'c0c0c0',
'medium_gray': '9e9e9e',
'dark_gray': '607d8b',
}
def color_names_to_rgb(apps, schema_editor):
RackRole = apps.get_model('dcim', 'RackRole')
DeviceRole = apps.get_model('dcim', 'DeviceRole')
for color_name, color_rgb in COLOR_CONVERSION.items():
RackRole.objects.filter(color=color_name).update(color=color_rgb)
DeviceRole.objects.filter(color=color_name).update(color=color_rgb)
def color_rgb_to_name(apps, schema_editor):
RackRole = apps.get_model('dcim', 'RackRole')
DeviceRole = apps.get_model('dcim', 'DeviceRole')
for color_name, color_rgb in COLOR_CONVERSION.items():
RackRole.objects.filter(color=color_rgb).update(color=color_name)
DeviceRole.objects.filter(color=color_rgb).update(color=color_name)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0021_add_ff_flexstack'),
]
operations = [
migrations.RunPython(color_names_to_rgb, color_rgb_to_name),
migrations.AlterField(
model_name='devicerole',
name='color',
field=utilities.fields.ColorField(max_length=6),
),
migrations.AlterField(
model_name='rackrole',
name='color',
field=utilities.fields.ColorField(max_length=6),
),
]

View File

@ -3,7 +3,7 @@ from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
@ -12,7 +12,7 @@ from django.db.models import Count, Q, ObjectDoesNotExist
from extras.models import CustomFieldModel, CustomField, CustomFieldValue from extras.models import CustomFieldModel, CustomField, CustomFieldValue
from extras.rpc import RPC_CLIENTS from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.fields import NullableCharField from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
@ -54,29 +54,6 @@ SUBDEVICE_ROLE_CHOICES = (
(SUBDEVICE_ROLE_CHILD, 'Child'), (SUBDEVICE_ROLE_CHILD, 'Child'),
) )
COLOR_TEAL = 'teal'
COLOR_GREEN = 'green'
COLOR_BLUE = 'blue'
COLOR_PURPLE = 'purple'
COLOR_YELLOW = 'yellow'
COLOR_ORANGE = 'orange'
COLOR_RED = 'red'
COLOR_GRAY1 = 'light_gray'
COLOR_GRAY2 = 'medium_gray'
COLOR_GRAY3 = 'dark_gray'
ROLE_COLOR_CHOICES = [
[COLOR_TEAL, 'Teal'],
[COLOR_GREEN, 'Green'],
[COLOR_BLUE, 'Blue'],
[COLOR_PURPLE, 'Purple'],
[COLOR_YELLOW, 'Yellow'],
[COLOR_ORANGE, 'Orange'],
[COLOR_RED, 'Red'],
[COLOR_GRAY1, 'Light Gray'],
[COLOR_GRAY2, 'Medium Gray'],
[COLOR_GRAY3, 'Dark Gray'],
]
# Virtual # Virtual
IFACE_FF_VIRTUAL = 0 IFACE_FF_VIRTUAL = 0
# Ethernet # Ethernet
@ -345,7 +322,7 @@ class RackRole(models.Model):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES) color = ColorField()
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -761,7 +738,7 @@ class DeviceRole(models.Model):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES) color = ColorField()
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -1173,16 +1150,13 @@ class Interface(models.Model):
return None return None
def get_connected_interface(self): def get_connected_interface(self):
try: connection = InterfaceConnection.objects.select_related().filter(Q(interface_a=self) | Q(interface_b=self))\
connection = InterfaceConnection.objects.select_related().get(Q(interface_a=self) | Q(interface_b=self)) .first()
if connection.interface_a == self: if connection and connection.interface_a == self:
return connection.interface_b return connection.interface_b
else: elif connection:
return connection.interface_a return connection.interface_a
except InterfaceConnection.DoesNotExist: return None
return None
except InterfaceConnection.MultipleObjectsReturned:
raise MultipleObjectsReturned("Multiple connections found for {} interface {}!".format(self.device, self))
class InterfaceConnection(models.Model): class InterfaceConnection(models.Model):

View File

@ -11,7 +11,7 @@ from .models import (
COLOR_LABEL = """ COLOR_LABEL = """
<label class="label {{ record.color }}">{{ record }}</label> <label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
""" """
DEVICE_LINK = """ DEVICE_LINK = """
@ -34,7 +34,7 @@ RACKROLE_ACTIONS = """
RACK_ROLE = """ RACK_ROLE = """
{% if record.role %} {% if record.role %}
<label class="label {{ record.role.color }}">{{ value }}</label> <label class="label" style="background-color: #{{ record.role.color }}">{{ value }}</label>
{% else %} {% else %}
&mdash; &mdash;
{% endif %} {% endif %}
@ -59,7 +59,7 @@ PLATFORM_ACTIONS = """
""" """
DEVICE_ROLE = """ DEVICE_ROLE = """
<label class="label {{ record.device_role.color }}">{{ value }}</label> <label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
""" """
STATUS_ICON = """ STATUS_ICON = """

View File

@ -1,5 +1,6 @@
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.safestring import mark_safe
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction
@ -54,4 +55,7 @@ class TopologyMapAdmin(admin.ModelAdmin):
@admin.register(UserAction) @admin.register(UserAction)
class UserActionAdmin(admin.ModelAdmin): class UserActionAdmin(admin.ModelAdmin):
actions = None actions = None
list_display = ['user', 'action', 'content_type', 'object_id', 'message'] list_display = ['user', 'action', 'content_type', 'object_id', '_message']
def _message(self, obj):
return mark_safe(obj.message)

View File

@ -130,7 +130,7 @@ class CustomField(models.Model):
if self.type == CF_TYPE_SELECT: if self.type == CF_TYPE_SELECT:
# Could be ModelChoiceField or TypedChoiceField # Could be ModelChoiceField or TypedChoiceField
return str(value.id) if hasattr(value, 'id') else str(value) return str(value.id) if hasattr(value, 'id') else str(value)
return str(value) return value
def deserialize_value(self, serialized_value): def deserialize_value(self, serialized_value):
""" """
@ -165,7 +165,7 @@ class CustomFieldValue(models.Model):
unique_together = ['field', 'obj_type', 'obj_id'] unique_together = ['field', 'obj_type', 'obj_id']
def __unicode__(self): def __unicode__(self):
return '{} {}'.format(self.obj, self.field) return u'{} {}'.format(self.obj, self.field)
@property @property
def value(self): def value(self):

View File

@ -28,7 +28,7 @@ class RIRAdmin(admin.ModelAdmin):
prepopulated_fields = { prepopulated_fields = {
'slug': ['name'], 'slug': ['name'],
} }
list_display = ['name', 'slug'] list_display = ['name', 'slug', 'is_private']
@admin.register(Aggregate) @admin.register(Aggregate)

View File

@ -58,13 +58,13 @@ class RIRSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = RIR model = RIR
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug', 'is_private']
class RIRNestedSerializer(RIRSerializer): class RIRNestedSerializer(RIRSerializer):
class Meta(RIRSerializer.Meta): class Meta(RIRSerializer.Meta):
pass fields = ['id', 'name', 'slug']
# #

View File

@ -46,6 +46,13 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
fields = ['name', 'rd'] fields = ['name', 'rd']
class RIRFilter(django_filters.FilterSet):
class Meta:
model = RIR
fields = ['is_private']
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
q = django_filters.MethodFilter( q = django_filters.MethodFilter(
action='search', action='search',

View File

@ -43,7 +43,8 @@
"pk": 1, "pk": 1,
"fields": { "fields": {
"name": "ARIN", "name": "ARIN",
"slug": "arin" "slug": "arin",
"is_private": false
} }
}, },
{ {
@ -51,7 +52,8 @@
"pk": 2, "pk": 2,
"fields": { "fields": {
"name": "RIPE", "name": "RIPE",
"slug": "ripe" "slug": "ripe",
"is_private": false
} }
}, },
{ {
@ -59,7 +61,8 @@
"pk": 3, "pk": 3,
"fields": { "fields": {
"name": "APNIC", "name": "APNIC",
"slug": "apnic" "slug": "apnic",
"is_private": false
} }
}, },
{ {
@ -67,7 +70,8 @@
"pk": 4, "pk": 4,
"fields": { "fields": {
"name": "LACNIC", "name": "LACNIC",
"slug": "lacnic" "slug": "lacnic",
"is_private": false
} }
}, },
{ {
@ -75,7 +79,8 @@
"pk": 5, "pk": 5,
"fields": { "fields": {
"name": "AFRINIC", "name": "AFRINIC",
"slug": "afrinic" "slug": "afrinic",
"is_private": false
} }
}, },
{ {
@ -83,7 +88,8 @@
"pk": 6, "pk": 6,
"fields": { "fields": {
"name": "RFC 1918", "name": "RFC 1918",
"slug": "rfc-1918" "slug": "rfc-1918",
"is_private": true
} }
}, },
{ {

View File

@ -75,7 +75,15 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
class Meta: class Meta:
model = RIR model = RIR
fields = ['name', 'slug'] fields = ['name', 'slug', 'is_private']
class RIRFilterForm(forms.Form, BootstrapMixin):
is_private = forms.NullBooleanField(required=False, label='Private', widget=forms.Select(choices=[
('', '---------'),
('True', 'Yes'),
('False', 'No'),
]))
# #

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-12-06 18:27
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0010_ipaddress_help_texts'),
]
operations = [
migrations.AddField(
model_name='rir',
name='is_private',
field=models.BooleanField(default=False, help_text=b'IP space managed by this RIR is considered private', verbose_name=b'Private'),
),
]

View File

@ -103,6 +103,8 @@ class RIR(models.Model):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
is_private = models.BooleanField(default=False, verbose_name='Private',
help_text='IP space managed by this RIR is considered private')
class Meta: class Meta:
ordering = ['name'] ordering = ['name']

View File

@ -126,6 +126,7 @@ class VRFTable(BaseTable):
class RIRTable(BaseTable): class RIRTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
is_private = tables.BooleanColumn(verbose_name='Private')
aggregate_count = tables.Column(verbose_name='Aggregates') aggregate_count = tables.Column(verbose_name='Aggregates')
stats_total = tables.Column(accessor='stats.total', verbose_name='Total', stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
footer=lambda table: sum(r.stats['total'] for r in table.data)) footer=lambda table: sum(r.stats['total'] for r in table.data))
@ -142,7 +143,8 @@ class RIRTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RIR model = RIR
fields = ('pk', 'name', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', 'stats_deprecated', 'stats_available', 'utilization', 'actions') fields = ('pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
'stats_deprecated', 'stats_available', 'utilization', 'actions')
# #

View File

@ -154,6 +154,8 @@ class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class RIRListView(ObjectListView): class RIRListView(ObjectListView):
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')) queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter
filter_form = forms.RIRFilterForm
table = tables.RIRTable table = tables.RIRTable
edit_permissions = ['ipam.change_rir', 'ipam.delete_rir'] edit_permissions = ['ipam.change_rir', 'ipam.delete_rir']
template_name = 'ipam/rir_list.html' template_name = 'ipam/rir_list.html'

View File

@ -12,7 +12,7 @@ except ImportError:
"the documentation.") "the documentation.")
VERSION = '1.7.1' VERSION = '1.7.2'
# Import local configuration # Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@ -188,7 +188,7 @@ REST_FRAMEWORK = {
# Swagger settings (API docs) # Swagger settings (API docs)
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'base_path': '{}/api/docs'.format(ALLOWED_HOSTS[0]), 'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH),
} }

View File

@ -98,7 +98,7 @@ nav ul.pagination {
div.rack_header { div.rack_header {
margin-left: 36px; margin-left: 36px;
text-align: center; text-align: center;
width: 200px; width: 230px;
} }
ul.rack_legend { ul.rack_legend {
float: left; float: left;
@ -126,29 +126,16 @@ ul.rack {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
position: absolute; position: absolute;
width: 200px; width: 230px;
} }
ul.rack li { ul.rack li {
border-top: 1px solid #e0e0e0;
display: block; display: block;
font-size: 13px; font-size: 13px;
height: 20px; height: 20px;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
} }
ul.rack_empty li {
background-color: #f7f7f7;
border-bottom: 1px solid #dddddd;
height: 20px;
}
ul.rack li.empty:last-child {
border-bottom: 0;
}
ul.rack_far_face {
z-index: 100;
}
ul.rack_near_face {
z-index: 200;
}
ul.rack li.h2u { height: 40px; } ul.rack li.h2u { height: 40px; }
ul.rack li.h2u a, ul.rack li.h2u span { padding: 10px 0; } ul.rack li.h2u a, ul.rack li.h2u span { padding: 10px 0; }
ul.rack li.h3u { height: 60px; } ul.rack li.h3u { height: 60px; }
@ -247,22 +234,9 @@ ul.rack li.h49u { height: 980px; }
ul.rack li.h49u a, ul.rack li.h49u span { padding: 480px 0; } ul.rack li.h49u a, ul.rack li.h49u span { padding: 480px 0; }
ul.rack li.h50u { height: 1000px; } ul.rack li.h50u { height: 1000px; }
ul.rack li.h50u a, ul.rack li.h50u span { padding: 490px 0; } ul.rack li.h50u a, ul.rack li.h50u span { padding: 490px 0; }
ul.rack li.occupied a { ul.rack_far_face {
color: #ffffff; background-color: #f7f7f7;
display: block; z-index: 100;
font-weight: bold;
}
ul.rack li.occupied a:hover {
text-decoration: none;
}
ul.rack li.occupied span {
display: block;
}
ul.rack_near_face li.empty {
border-bottom: 1px solid #e0e0e0;
}
ul.rack_near_face li.occupied {
color: #474747;
} }
ul.rack_far_face li.occupied { ul.rack_far_face li.occupied {
background: repeating-linear-gradient( background: repeating-linear-gradient(
@ -272,7 +246,6 @@ ul.rack_far_face li.occupied {
#f0f0f0 7px, #f0f0f0 7px,
#f0f0f0 14px #f0f0f0 14px
); );
color: #303030;
} }
ul.rack_far_face li.blocked { ul.rack_far_face li.blocked {
background: repeating-linear-gradient( background: repeating-linear-gradient(
@ -282,54 +255,46 @@ ul.rack_far_face li.blocked {
#ffc7c7 7px, #ffc7c7 7px,
#ffc7c7 14px #ffc7c7 14px
); );
border-bottom: 1px solid #e0e0e0;
color: #303030;
} }
ul.rack_near_face li.empty a { ul.rack_near_face {
z-index: 200;
}
ul.rack_near_face li.occupied {
border-top: 1px solid #474747;
color: #474747;
}
ul.rack_near_face li.occupied:hover {
background-image: url('../img/tint_20.png');
}
ul.rack_near_face li:first-child {
border-top: 0;
}
ul.rack_near_face li.available a {
color: #0000ff; color: #0000ff;
display: none; display: none;
text-decoration: none; text-decoration: none;
} }
ul.rack_near_face li.empty:hover { ul.rack_near_face li.available:hover {
background-color: #ffffff; background-color: #ffffff;
} }
ul.rack_near_face li.empty:hover a { ul.rack_near_face li.available:hover a {
display: block; display: block;
} }
ul.rack li.occupied a {
/* Colors (from http://flatuicolors.com) */ color: #ffffff;
.teal { background-color: #1abc9c; } display: block;
.green { background-color: #2ecc71; } font-weight: bold;
.blue { background-color: #3498db; } }
.purple { background-color: #9b59b6; } ul.rack li.occupied a:hover {
.yellow { background-color: #f1c40f; } text-decoration: none;
.orange { background-color: #e67e22; } }
.red { background-color: #e74c3c; } ul.rack li.occupied span {
.light_gray { background-color: #dce2e3; } cursor: default;
.medium_gray { background-color: #95a5a6; } display: block;
.dark_gray { background-color: #34495e; } }
li.occupied + li.available {
/* Rack elevation coloring */ border-top: 1px solid #474747;
ul.rack .teal { border-bottom: 1px solid #16a085; } }
ul.rack .teal:hover { background-color: #16a085; }
ul.rack .green { border-bottom: 1px solid #27ae60; }
ul.rack .green:hover { background-color: #27ae60; }
ul.rack .blue { border-bottom: 1px solid #2980b9; }
ul.rack .blue:hover { background-color: #2980b9; }
ul.rack .purple { border-bottom: 1px solid #8e44ad; }
ul.rack .purple:hover { background-color: #8e44ad; }
ul.rack .yellow { border-bottom: 1px solid #f39c12; }
ul.rack .yellow:hover { background-color: #f39c12; }
ul.rack .orange { border-bottom: 1px solid #d35400; }
ul.rack .orange:hover { background-color: #d35400; }
ul.rack .red { border-bottom: 1px solid #c0392b; }
ul.rack .red:hover { background-color: #c0392b; }
ul.rack .light_gray { border-bottom: 1px solid #bdc3c7; }
ul.rack .light_gray:hover { background-color: #bdc3c7; }
ul.rack .medium_gray { border-bottom: 1px solid #7f8c8d; }
ul.rack .medium_gray:hover { background-color: #7f8c8d; }
ul.rack .dark_gray { border-bottom: 1px solid #2c3e50; }
ul.rack .dark_gray:hover { background-color: #2c3e50; }
/* Misc */ /* Misc */
.banner-bottom { .banner-bottom {

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

View File

@ -6,13 +6,6 @@
<div class="rack_frame"> <div class="rack_frame">
<!-- Render all slots empty -->
<ul class="rack rack_empty">
{% for u in rack.units %}
<li></li>
{% endfor %}
</ul>
<!-- Render rear view of devices on far face --> <!-- Render rear view of devices on far face -->
<ul class="rack rack_far_face"> <ul class="rack rack_far_face">
{% for u in secondary_face %} {% for u in secondary_face %}
@ -42,7 +35,7 @@
{% endifequal %} {% endifequal %}
</li> </li>
{% else %} {% else %}
<li class="empty"> <li class="available">
{% if perms.dcim.add_device %} {% if perms.dcim.add_device %}
<a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}&face={{ face_id }}&position={{ u.id }}" class="add_device" >add device</a> <a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}&face={{ face_id }}&position={{ u.id }}" class="add_device" >add device</a>
{% endif %} {% endif %}

View File

@ -1,7 +1,7 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Assign an IP Address{% endblock %} {% block title %}Assign a New IP Address{% endblock %}
{% block content %} {% block content %}
<form action="." method="post" class="form form-horizontal"> <form action="." method="post" class="form form-horizontal">
@ -40,6 +40,7 @@
</div> </div>
</div> </div>
{% render_field form.interface %} {% render_field form.interface %}
{% render_field form.set_as_primary %}
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -3,7 +3,7 @@
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li> <li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
{% if prefix.vrf %} {% if prefix.vrf %}
<li><a href="{% url 'ipam:prefix_list' %}?vrf={{ prefix.vrf.pk }}">{{ prefix.vrf }}</a></li> <li><a href="{% url 'ipam:vrf' pk=prefix.vrf.pk %}">{{ prefix.vrf }}</a></li>
{% endif %} {% endif %}
<li>{{ prefix }}</li> <li>{{ prefix }}</li>
</ol> </ol>

View File

@ -9,7 +9,7 @@
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li> <li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
{% if ipaddress.vrf %} {% if ipaddress.vrf %}
<li><a href="{% url 'ipam:ipaddress_list' %}?vrf={{ ipaddress.vrf.pk }}">{{ ipaddress.vrf }}</a></li> <li><a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a></li>
{% endif %} {% endif %}
<li>{{ ipaddress }}</li> <li>{{ ipaddress }}</li>
</ol> </ol>

View File

@ -2,7 +2,7 @@
{% load static from staticfiles %} {% load static from staticfiles %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Assign IP Address{% endblock %} {% block title %}Assign an IP Address{% endblock %}
{% block content %} {% block content %}
<form action="." method="post" class="form form-horizontal"> <form action="." method="post" class="form form-horizontal">
@ -19,9 +19,25 @@
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Assign IP Address {{ ipaddress }} ({% if ipaddress.vrf %}VRF {{ ipaddress.vrf }}{% else %}Global Table{% endif %})</strong> <strong>Assign an IP Address</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label">IP Address</label>
<div class="col-md-9">
<p class="form-control-static">{{ ipaddress }}</p>
</div>
<label class="col-md-3 control-label">VRF</label>
<div class="col-md-9">
<p class="form-control-static">
{% if ipaddress.vrf %}
<a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a> ({{ ipaddress.vrf.rd }})
{% else %}
<span>Global</span>
{% endif %}
</p>
</div>
</div>
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li> <li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li> <li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>

View File

@ -12,7 +12,7 @@
IPv4 Stats IPv4 Stats
</a> </a>
{% else %} {% else %}
<a href="{% url 'ipam:rir_list' %}?family=6" class="btn btn-default"> <a href="{% url 'ipam:rir_list' %}?family=6{% if request.GET %}&{{ request.GET.urlencode }}{% endif %}" class="btn btn-default">
<span class="fa fa-table" aria-hidden="true"></span> <span class="fa fa-table" aria-hidden="true"></span>
IPv6 Stats IPv6 Stats
</a> </a>
@ -26,11 +26,14 @@
</div> </div>
<h1>RIRs</h1> <h1>RIRs</h1>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}
{% if request.GET.family == '6' %}
<div class="alert alert-info pull-right"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
{% endif %}
</div> </div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
</div>
</div> </div>
{% if request.GET.family == '6' %}
<div class="pull-right text-muted"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -8,7 +8,9 @@
<div class="col-md-9"> <div class="col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></li> <li><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></li>
<li><a href="{% url 'tenancy:tenant_list' %}?group={{ tenant.group.slug }}">{{ tenant.group }}</a></li> {% if tenant.group %}
<li><a href="{% url 'tenancy:tenant_list' %}?group={{ tenant.group.slug }}">{{ tenant.group }}</a></li>
{% endif %}
<li>{{ tenant }}</li> <li>{{ tenant }}</li>
</ol> </ol>
</div> </div>
@ -50,7 +52,11 @@
<tr> <tr>
<td>Group</td> <td>Group</td>
<td> <td>
<a href="{{ tenant.group.get_absolute_url }}">{{ tenant.group }}</a> {% if tenant.group %}
<a href="{{ tenant.group.get_absolute_url }}">{{ tenant.group }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -40,7 +40,7 @@
</tr> </tr>
<tr> <tr>
<td>Group</td> <td>Group</td>
<td>Tenant group</td> <td>Tenant group (optional)</td>
<td>Customers</td> <td>Customers</td>
</tr> </tr>
<tr> <tr>

View File

@ -48,6 +48,6 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
return ','.join([ return ','.join([
self.name, self.name,
self.slug, self.slug,
self.group.name, self.group.name if self.group else '',
self.description, self.description,
]) ])

View File

@ -1,5 +1,11 @@
from django.core.validators import RegexValidator
from django.db import models from django.db import models
from .forms import ColorSelect
validate_color = RegexValidator('^[0-9a-f]{6}$', 'Enter a valid hexadecimal RGB color code.', 'invalid')
class NullableCharField(models.CharField): class NullableCharField(models.CharField):
description = "Stores empty values as NULL rather than ''" description = "Stores empty values as NULL rather than ''"
@ -11,3 +17,16 @@ class NullableCharField(models.CharField):
def get_prep_value(self, value): def get_prep_value(self, value):
return value or None return value or None
class ColorField(models.CharField):
default_validators = [validate_color]
description = "A hexadecimal RGB color code"
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 6
super(ColorField, self).__init__(*args, **kwargs)
def formfield(self, **kwargs):
kwargs['widget'] = ColorSelect
return super(ColorField, self).formfield(**kwargs)

View File

@ -11,6 +11,32 @@ from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
COLOR_CHOICES = (
('aa1409', 'Dark red'),
('f44336', 'Red'),
('e91e63', 'Pink'),
('ff66ff', 'Fuschia'),
('9c27b0', 'Purple'),
('673ab7', 'Dark purple'),
('3f51b5', 'Indigo'),
('2196f3', 'Blue'),
('03a9f4', 'Light blue'),
('00bcd4', 'Cyan'),
('009688', 'Teal'),
('2f6a31', 'Dark green'),
('4caf50', 'Green'),
('8bc34a', 'Light green'),
('cddc39', 'Lime'),
('ffeb3b', 'Yellow'),
('ffc107', 'Amber'),
('ff9800', 'Orange'),
('ff5722', 'Dark orange'),
('795548', 'Brown'),
('c0c0c0', 'Light grey'),
('9e9e9e', 'Grey'),
('607d8b', 'Dark grey'),
('111111', 'Black'),
)
NUMERIC_EXPANSION_PATTERN = '\[(\d+-\d+)\]' NUMERIC_EXPANSION_PATTERN = '\[(\d+-\d+)\]'
IP4_EXPANSION_PATTERN = '\[([0-9]{1,3}-[0-9]{1,3})\]' IP4_EXPANSION_PATTERN = '\[([0-9]{1,3}-[0-9]{1,3})\]'
IP6_EXPANSION_PATTERN = '\[([0-9a-f]{1,4}-[0-9a-f]{1,4})\]' IP6_EXPANSION_PATTERN = '\[([0-9a-f]{1,4}-[0-9a-f]{1,4})\]'
@ -71,6 +97,27 @@ class SmallTextarea(forms.Textarea):
pass pass
class ColorSelect(forms.Select):
def __init__(self, *args, **kwargs):
kwargs['choices'] = COLOR_CHOICES
super(ColorSelect, self).__init__(*args, **kwargs)
def render_option(self, selected_choices, option_value, option_label):
if option_value is None:
option_value = ''
option_value = force_text(option_value)
if option_value in selected_choices:
selected_html = mark_safe(' selected')
if not self.allow_multiple_selected:
# Only allow for a single selection.
selected_choices.remove(option_value)
else:
selected_html = ''
return format_html('<option value="{}"{} style="background-color: #{}">{}</option>',
option_value, selected_html, option_value, force_text(option_label))
class SelectWithDisabled(forms.Select): class SelectWithDisabled(forms.Select):
""" """
Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include
@ -234,6 +281,7 @@ class CommentField(forms.CharField):
A textarea with support for GitHub-Flavored Markdown. Exists mostly just to add a standard help_text. A textarea with support for GitHub-Flavored Markdown. Exists mostly just to add a standard help_text.
""" """
widget = forms.Textarea widget = forms.Textarea
default_label = 'Comments'
# TODO: Port GFM syntax cheat sheet to internal documentation # TODO: Port GFM syntax cheat sheet to internal documentation
default_helptext = '<i class="fa fa-info-circle"></i> '\ default_helptext = '<i class="fa fa-info-circle"></i> '\
'<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">'\ '<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">'\
@ -241,8 +289,9 @@ class CommentField(forms.CharField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
required = kwargs.pop('required', False) required = kwargs.pop('required', False)
label = kwargs.pop('label', self.default_label)
help_text = kwargs.pop('help_text', self.default_helptext) help_text = kwargs.pop('help_text', self.default_helptext)
super(CommentField, self).__init__(required=required, help_text=help_text, *args, **kwargs) super(CommentField, self).__init__(required=required, label=label, help_text=help_text, *args, **kwargs)
class FlexibleModelChoiceField(forms.ModelChoiceField): class FlexibleModelChoiceField(forms.ModelChoiceField):

View File

@ -1,3 +1,4 @@
cffi>=1.8
cryptography==1.4 cryptography==1.4
Django==1.10 Django==1.10
django-debug-toolbar==1.4 django-debug-toolbar==1.4