mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
commit
66be85a41f
@ -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
|
||||
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
|
||||
and add a thumbs up (+1). 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.
|
||||
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. (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
|
||||
feature creep. For example, the following features would be firmly out of scope for NetBox:
|
||||
|
@ -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:
|
||||
|
||||
```
|
||||
git clone -b master https://github.com/digitalocean/netbox.git
|
||||
cd netbox
|
||||
docker-compose up -d
|
||||
```no-highlight
|
||||
# git clone -b master https://github.com/digitalocean/netbox.git
|
||||
# cd netbox
|
||||
# docker-compose up -d
|
||||
```
|
||||
|
||||
The application will be available on http://localhost/ after a few minutes.
|
||||
|
@ -7,19 +7,19 @@ built-in Django users in the event of a failure.
|
||||
|
||||
On Ubuntu:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev
|
||||
```
|
||||
|
||||
On CentOS:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
sudo yum install -y python-devel openldap-devel
|
||||
```
|
||||
|
||||
## Install django-auth-ldap
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
sudo pip install django-auth-ldap
|
||||
```
|
||||
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# yum install -y epel-release
|
||||
# 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`.
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
||||
# tar -xzf vX.Y.Z.tar.gz -C /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`.
|
||||
|
||||
```
|
||||
# mkdir -p /opt/netbox/
|
||||
# cd /opt/netbox/
|
||||
```no-highlight
|
||||
# mkdir -p /opt/netbox/ && cd /opt/netbox/
|
||||
```
|
||||
|
||||
If `git` is not already installed, install it:
|
||||
|
||||
**Debian/Ubuntu**
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# apt-get install -y git
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# yum install -y git
|
||||
```
|
||||
|
||||
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 .
|
||||
Cloning into '.'...
|
||||
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.)
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# 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`.
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# cd netbox/netbox/
|
||||
# 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:
|
||||
|
||||
```
|
||||
```python
|
||||
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:
|
||||
|
||||
```
|
||||
```python
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'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):
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# cd /opt/netbox/netbox/
|
||||
# ./manage.py migrate
|
||||
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:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# ./manage.py createsuperuser
|
||||
Username: admin
|
||||
Email address: admin@example.com
|
||||
@ -155,7 +154,7 @@ Superuser created successfully.
|
||||
|
||||
# Collect Static Files
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# ./manage.py collectstatic
|
||||
|
||||
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
|
||||
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
|
||||
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:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# ./manage.py runserver 0.0.0.0:8000 --insecure
|
||||
Performing system checks...
|
||||
|
||||
|
@ -4,27 +4,27 @@ NetBox requires a PostgreSQL database to store data. MySQL is not supported, as
|
||||
|
||||
**Debian/Ubuntu**
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# apt-get install -y postgresql libpq-dev python-psycopg2
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# yum install -y postgresql postgresql-server postgresql-devel python-psycopg2
|
||||
# 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:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
host all all 127.0.0.1/32 md5
|
||||
host all all ::1/128 md5
|
||||
```
|
||||
|
||||
Then, start the service:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# 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
|
||||
DO NOT USE THE PASSWORD FROM THE EXAMPLE.
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# sudo -u postgres psql
|
||||
psql (9.3.13)
|
||||
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:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# psql -U netbox -h localhost -W
|
||||
```
|
||||
|
||||
|
@ -8,7 +8,7 @@ Download the [latest stable release](https://github.com/digitalocean/netbox/rele
|
||||
|
||||
Download and extract the latest version:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
||||
# tar -xzf vX.Y.Z.tar.gz -C /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:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# 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:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# 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:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# cd /opt/netbox
|
||||
# git checkout 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).
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# ./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`:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# sudo supervisorctl restart netbox
|
||||
```
|
||||
|
@ -5,7 +5,7 @@ We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for
|
||||
!!! 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.
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# 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.
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# 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`.)
|
||||
|
||||
```
|
||||
```nginx
|
||||
server {
|
||||
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.
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# cd /etc/nginx/sites-enabled/
|
||||
# rm default
|
||||
# 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.
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# service nginx restart
|
||||
```
|
||||
|
||||
@ -59,13 +59,13 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
|
||||
|
||||
## Option B: Apache
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# apt-get install -y apache2
|
||||
```
|
||||
|
||||
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
|
||||
|
||||
```
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
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:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# a2enmod proxy
|
||||
# a2enmod proxy_http
|
||||
# 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`.
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
command = '/usr/bin/gunicorn'
|
||||
pythonpath = '/opt/netbox/netbox'
|
||||
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.
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
[program:netbox]
|
||||
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
|
||||
directory = /opt/netbox/netbox/
|
||||
@ -124,7 +124,7 @@ user = www-data
|
||||
|
||||
Then, restart the supervisor service to detect and run the gunicorn service:
|
||||
|
||||
```
|
||||
```no-highlight
|
||||
# service supervisor restart
|
||||
```
|
||||
|
||||
|
@ -54,7 +54,7 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
portal_url = forms.URLField(required=False, label='Portal')
|
||||
noc_contact = forms.CharField(required=False, widget=SmallTextarea, label='NOC contact')
|
||||
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
|
||||
comments = CommentField()
|
||||
comments = CommentField(widget=SmallTextarea)
|
||||
|
||||
class Meta:
|
||||
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)
|
||||
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||
comments = CommentField()
|
||||
comments = CommentField(widget=SmallTextarea)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments']
|
||||
|
@ -1,4 +1,5 @@
|
||||
import django_filters
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
@ -146,6 +147,10 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
mac_address = django_filters.MethodFilter(
|
||||
action='_mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
@ -254,6 +259,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
Q(comments__icontains=value)
|
||||
).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):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
|
@ -5,7 +5,7 @@
|
||||
"fields": {
|
||||
"name": "Console Server",
|
||||
"slug": "console-server",
|
||||
"color": "teal"
|
||||
"color": "009688"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -14,7 +14,7 @@
|
||||
"fields": {
|
||||
"name": "Core Switch",
|
||||
"slug": "core-switch",
|
||||
"color": "blue"
|
||||
"color": "2196f3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -23,7 +23,7 @@
|
||||
"fields": {
|
||||
"name": "Distribution Switch",
|
||||
"slug": "distribution-switch",
|
||||
"color": "blue"
|
||||
"color": "2196f3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -32,7 +32,7 @@
|
||||
"fields": {
|
||||
"name": "Access Switch",
|
||||
"slug": "access-switch",
|
||||
"color": "blue"
|
||||
"color": "2196f3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -41,7 +41,7 @@
|
||||
"fields": {
|
||||
"name": "Management Switch",
|
||||
"slug": "management-switch",
|
||||
"color": "orange"
|
||||
"color": "ff9800"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -50,7 +50,7 @@
|
||||
"fields": {
|
||||
"name": "Firewall",
|
||||
"slug": "firewall",
|
||||
"color": "red"
|
||||
"color": "f44336"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -59,7 +59,7 @@
|
||||
"fields": {
|
||||
"name": "Router",
|
||||
"slug": "router",
|
||||
"color": "purple"
|
||||
"color": "9c27b0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -68,7 +68,7 @@
|
||||
"fields": {
|
||||
"name": "Server",
|
||||
"slug": "server",
|
||||
"color": "medium_gray"
|
||||
"color": "9e9e9e"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -77,7 +77,7 @@
|
||||
"fields": {
|
||||
"name": "PDU",
|
||||
"slug": "pdu",
|
||||
"color": "dark_gray"
|
||||
"color": "607d8b"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -221,7 +221,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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')
|
||||
u_height = forms.IntegerField(required=False, label='Height (U)')
|
||||
comments = CommentField()
|
||||
comments = CommentField(widget=SmallTextarea)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['group', 'tenant', 'role', 'comments']
|
||||
@ -612,6 +612,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')),
|
||||
to_field_name='slug', null_option=(0, 'None'))
|
||||
status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
|
||||
mac_address = forms.CharField(label='MAC address')
|
||||
|
||||
|
||||
#
|
||||
|
57
netbox/dcim/migrations/0022_color_names_to_rgb.py
Normal file
57
netbox/dcim/migrations/0022_color_names_to_rgb.py
Normal 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),
|
||||
),
|
||||
]
|
@ -3,7 +3,7 @@ from collections import OrderedDict
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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.validators import MaxValueValidator, MinValueValidator
|
||||
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.rpc import RPC_CLIENTS
|
||||
from tenancy.models import Tenant
|
||||
from utilities.fields import NullableCharField
|
||||
from utilities.fields import ColorField, NullableCharField
|
||||
from utilities.managers import NaturalOrderByManager
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
|
||||
@ -54,29 +54,6 @@ SUBDEVICE_ROLE_CHOICES = (
|
||||
(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
|
||||
IFACE_FF_VIRTUAL = 0
|
||||
# Ethernet
|
||||
@ -345,7 +322,7 @@ class RackRole(models.Model):
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
|
||||
color = ColorField()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@ -761,7 +738,7 @@ class DeviceRole(models.Model):
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
|
||||
color = ColorField()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@ -1173,16 +1150,13 @@ class Interface(models.Model):
|
||||
return None
|
||||
|
||||
def get_connected_interface(self):
|
||||
try:
|
||||
connection = InterfaceConnection.objects.select_related().get(Q(interface_a=self) | Q(interface_b=self))
|
||||
if connection.interface_a == self:
|
||||
return connection.interface_b
|
||||
else:
|
||||
return connection.interface_a
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
return None
|
||||
except InterfaceConnection.MultipleObjectsReturned:
|
||||
raise MultipleObjectsReturned("Multiple connections found for {} interface {}!".format(self.device, self))
|
||||
connection = InterfaceConnection.objects.select_related().filter(Q(interface_a=self) | Q(interface_b=self))\
|
||||
.first()
|
||||
if connection and connection.interface_a == self:
|
||||
return connection.interface_b
|
||||
elif connection:
|
||||
return connection.interface_a
|
||||
return None
|
||||
|
||||
|
||||
class InterfaceConnection(models.Model):
|
||||
|
@ -11,7 +11,7 @@ from .models import (
|
||||
|
||||
|
||||
COLOR_LABEL = """
|
||||
<label class="label {{ record.color }}">{{ record }}</label>
|
||||
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
|
||||
"""
|
||||
|
||||
DEVICE_LINK = """
|
||||
@ -34,7 +34,7 @@ RACKROLE_ACTIONS = """
|
||||
|
||||
RACK_ROLE = """
|
||||
{% if record.role %}
|
||||
<label class="label {{ record.role.color }}">{{ value }}</label>
|
||||
<label class="label" style="background-color: #{{ record.role.color }}">{{ value }}</label>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
@ -59,7 +59,7 @@ PLATFORM_ACTIONS = """
|
||||
"""
|
||||
|
||||
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 = """
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction
|
||||
|
||||
@ -54,4 +55,7 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
||||
@admin.register(UserAction)
|
||||
class UserActionAdmin(admin.ModelAdmin):
|
||||
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)
|
||||
|
@ -130,7 +130,7 @@ class CustomField(models.Model):
|
||||
if self.type == CF_TYPE_SELECT:
|
||||
# Could be ModelChoiceField or TypedChoiceField
|
||||
return str(value.id) if hasattr(value, 'id') else str(value)
|
||||
return str(value)
|
||||
return value
|
||||
|
||||
def deserialize_value(self, serialized_value):
|
||||
"""
|
||||
@ -165,7 +165,7 @@ class CustomFieldValue(models.Model):
|
||||
unique_together = ['field', 'obj_type', 'obj_id']
|
||||
|
||||
def __unicode__(self):
|
||||
return '{} {}'.format(self.obj, self.field)
|
||||
return u'{} {}'.format(self.obj, self.field)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
|
@ -28,7 +28,7 @@ class RIRAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
list_display = ['name', 'slug', 'is_private']
|
||||
|
||||
|
||||
@admin.register(Aggregate)
|
||||
|
@ -58,13 +58,13 @@ class RIRSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ['id', 'name', 'slug']
|
||||
fields = ['id', 'name', 'slug', 'is_private']
|
||||
|
||||
|
||||
class RIRNestedSerializer(RIRSerializer):
|
||||
|
||||
class Meta(RIRSerializer.Meta):
|
||||
pass
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
|
@ -46,6 +46,13 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
fields = ['name', 'rd']
|
||||
|
||||
|
||||
class RIRFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ['is_private']
|
||||
|
||||
|
||||
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
|
@ -43,7 +43,8 @@
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "ARIN",
|
||||
"slug": "arin"
|
||||
"slug": "arin",
|
||||
"is_private": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -51,7 +52,8 @@
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "RIPE",
|
||||
"slug": "ripe"
|
||||
"slug": "ripe",
|
||||
"is_private": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -59,7 +61,8 @@
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "APNIC",
|
||||
"slug": "apnic"
|
||||
"slug": "apnic",
|
||||
"is_private": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -67,7 +70,8 @@
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "LACNIC",
|
||||
"slug": "lacnic"
|
||||
"slug": "lacnic",
|
||||
"is_private": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -75,7 +79,8 @@
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "AFRINIC",
|
||||
"slug": "afrinic"
|
||||
"slug": "afrinic",
|
||||
"is_private": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -83,7 +88,8 @@
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "RFC 1918",
|
||||
"slug": "rfc-1918"
|
||||
"slug": "rfc-1918",
|
||||
"is_private": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -75,7 +75,15 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
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'),
|
||||
]))
|
||||
|
||||
|
||||
#
|
||||
|
20
netbox/ipam/migrations/0011_rir_add_is_private.py
Normal file
20
netbox/ipam/migrations/0011_rir_add_is_private.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -103,6 +103,8 @@ class RIR(models.Model):
|
||||
"""
|
||||
name = models.CharField(max_length=50, 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:
|
||||
ordering = ['name']
|
||||
|
@ -126,6 +126,7 @@ class VRFTable(BaseTable):
|
||||
class RIRTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
is_private = tables.BooleanColumn(verbose_name='Private')
|
||||
aggregate_count = tables.Column(verbose_name='Aggregates')
|
||||
stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
|
||||
footer=lambda table: sum(r.stats['total'] for r in table.data))
|
||||
@ -142,7 +143,8 @@ class RIRTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
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')
|
||||
|
||||
|
||||
#
|
||||
|
@ -154,6 +154,8 @@ class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class RIRListView(ObjectListView):
|
||||
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||
filter = filters.RIRFilter
|
||||
filter_form = forms.RIRFilterForm
|
||||
table = tables.RIRTable
|
||||
edit_permissions = ['ipam.change_rir', 'ipam.delete_rir']
|
||||
template_name = 'ipam/rir_list.html'
|
||||
|
@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.7.1'
|
||||
VERSION = '1.7.2'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
@ -188,7 +188,7 @@ REST_FRAMEWORK = {
|
||||
|
||||
# Swagger settings (API docs)
|
||||
SWAGGER_SETTINGS = {
|
||||
'base_path': '{}/api/docs'.format(ALLOWED_HOSTS[0]),
|
||||
'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH),
|
||||
}
|
||||
|
||||
|
||||
|
@ -98,7 +98,7 @@ nav ul.pagination {
|
||||
div.rack_header {
|
||||
margin-left: 36px;
|
||||
text-align: center;
|
||||
width: 200px;
|
||||
width: 230px;
|
||||
}
|
||||
ul.rack_legend {
|
||||
float: left;
|
||||
@ -126,29 +126,16 @@ ul.rack {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
width: 230px;
|
||||
}
|
||||
ul.rack li {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
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 a, ul.rack li.h2u span { padding: 10px 0; }
|
||||
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.h50u { height: 1000px; }
|
||||
ul.rack li.h50u a, ul.rack li.h50u span { padding: 490px 0; }
|
||||
ul.rack li.occupied a {
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
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 {
|
||||
background-color: #f7f7f7;
|
||||
z-index: 100;
|
||||
}
|
||||
ul.rack_far_face li.occupied {
|
||||
background: repeating-linear-gradient(
|
||||
@ -272,7 +246,6 @@ ul.rack_far_face li.occupied {
|
||||
#f0f0f0 7px,
|
||||
#f0f0f0 14px
|
||||
);
|
||||
color: #303030;
|
||||
}
|
||||
ul.rack_far_face li.blocked {
|
||||
background: repeating-linear-gradient(
|
||||
@ -282,54 +255,46 @@ ul.rack_far_face li.blocked {
|
||||
#ffc7c7 7px,
|
||||
#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;
|
||||
display: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.rack_near_face li.empty:hover {
|
||||
ul.rack_near_face li.available:hover {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
ul.rack_near_face li.empty:hover a {
|
||||
ul.rack_near_face li.available:hover a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Colors (from http://flatuicolors.com) */
|
||||
.teal { background-color: #1abc9c; }
|
||||
.green { background-color: #2ecc71; }
|
||||
.blue { background-color: #3498db; }
|
||||
.purple { background-color: #9b59b6; }
|
||||
.yellow { background-color: #f1c40f; }
|
||||
.orange { background-color: #e67e22; }
|
||||
.red { background-color: #e74c3c; }
|
||||
.light_gray { background-color: #dce2e3; }
|
||||
.medium_gray { background-color: #95a5a6; }
|
||||
.dark_gray { background-color: #34495e; }
|
||||
|
||||
/* Rack elevation coloring */
|
||||
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; }
|
||||
ul.rack li.occupied a {
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
ul.rack li.occupied a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.rack li.occupied span {
|
||||
cursor: default;
|
||||
display: block;
|
||||
}
|
||||
li.occupied + li.available {
|
||||
border-top: 1px solid #474747;
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
.banner-bottom {
|
||||
|
BIN
netbox/project-static/img/tint_20.png
Normal file
BIN
netbox/project-static/img/tint_20.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 B |
@ -6,13 +6,6 @@
|
||||
|
||||
<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 -->
|
||||
<ul class="rack rack_far_face">
|
||||
{% for u in secondary_face %}
|
||||
@ -42,7 +35,7 @@
|
||||
{% endifequal %}
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="empty">
|
||||
<li class="available">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Assign an IP Address{% endblock %}
|
||||
{% block title %}Assign a New IP Address{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
@ -40,6 +40,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
{% render_field form.set_as_primary %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
|
@ -3,7 +3,7 @@
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
|
||||
{% 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 %}
|
||||
<li>{{ prefix }}</li>
|
||||
</ol>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
|
||||
{% 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 %}
|
||||
<li>{{ ipaddress }}</li>
|
||||
</ol>
|
||||
|
@ -2,7 +2,7 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Assign IP Address{% endblock %}
|
||||
{% block title %}Assign an IP Address{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
@ -19,9 +19,25 @@
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<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 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">
|
||||
<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>
|
||||
|
@ -12,7 +12,7 @@
|
||||
IPv4 Stats
|
||||
</a>
|
||||
{% 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>
|
||||
IPv6 Stats
|
||||
</a>
|
||||
@ -26,11 +26,14 @@
|
||||
</div>
|
||||
<h1>RIRs</h1>
|
||||
<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' %}
|
||||
{% if request.GET.family == '6' %}
|
||||
<div class="alert alert-info pull-right"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/filter_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% if request.GET.family == '6' %}
|
||||
<div class="pull-right text-muted"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -8,7 +8,9 @@
|
||||
<div class="col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<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>
|
||||
</ol>
|
||||
</div>
|
||||
@ -50,7 +52,11 @@
|
||||
<tr>
|
||||
<td>Group</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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -40,7 +40,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Tenant group</td>
|
||||
<td>Tenant group (optional)</td>
|
||||
<td>Customers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -48,6 +48,6 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
|
||||
return ','.join([
|
||||
self.name,
|
||||
self.slug,
|
||||
self.group.name,
|
||||
self.group.name if self.group else '',
|
||||
self.description,
|
||||
])
|
||||
|
@ -1,5 +1,11 @@
|
||||
from django.core.validators import RegexValidator
|
||||
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):
|
||||
description = "Stores empty values as NULL rather than ''"
|
||||
@ -11,3 +17,16 @@ class NullableCharField(models.CharField):
|
||||
|
||||
def get_prep_value(self, value):
|
||||
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)
|
||||
|
@ -11,6 +11,32 @@ from django.utils.html import format_html
|
||||
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+)\]'
|
||||
IP4_EXPANSION_PATTERN = '\[([0-9]{1,3}-[0-9]{1,3})\]'
|
||||
IP6_EXPANSION_PATTERN = '\[([0-9a-f]{1,4}-[0-9a-f]{1,4})\]'
|
||||
@ -71,6 +97,27 @@ class SmallTextarea(forms.Textarea):
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
widget = forms.Textarea
|
||||
default_label = 'Comments'
|
||||
# TODO: Port GFM syntax cheat sheet to internal documentation
|
||||
default_helptext = '<i class="fa fa-info-circle"></i> '\
|
||||
'<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):
|
||||
required = kwargs.pop('required', False)
|
||||
label = kwargs.pop('label', self.default_label)
|
||||
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):
|
||||
|
@ -1,3 +1,4 @@
|
||||
cffi>=1.8
|
||||
cryptography==1.4
|
||||
Django==1.10
|
||||
django-debug-toolbar==1.4
|
||||
|
Loading…
Reference in New Issue
Block a user