mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-20 00:56:59 +01:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eb140fd65 | ||
|
|
a68e82575f | ||
|
|
5035a9567b | ||
|
|
d5095362d7 | ||
|
|
3a6d7a1f7f | ||
|
|
cc6ae8ebe4 | ||
|
|
b4940a64be | ||
|
|
fca812928e | ||
|
|
4a9b4c5387 | ||
|
|
1f09f3d096 | ||
|
|
efb95937fc | ||
|
|
ce7ee1771a | ||
|
|
da216e2c22 | ||
|
|
e58ee4e0e3 | ||
|
|
66be85a41f | ||
|
|
2171dcee7f | ||
|
|
3262262a8a | ||
|
|
28b586aca7 | ||
|
|
f007b0dbde | ||
|
|
6e5950be77 | ||
|
|
eb4cd0e723 | ||
|
|
300ee820fa | ||
|
|
7d6d7942d9 | ||
|
|
05debf7e40 | ||
|
|
dc88cb5ac7 | ||
|
|
b275009544 | ||
|
|
d960481adb | ||
|
|
2986840755 | ||
|
|
9b8bae501b | ||
|
|
9ea3383fde | ||
|
|
77ac79f32c | ||
|
|
e31fae5ec5 | ||
|
|
8bff8bcbe2 | ||
|
|
cc79b1136b | ||
|
|
1af9ea9e2d | ||
|
|
814c11167e | ||
|
|
1d509a8ff8 | ||
|
|
f2232a15d9 | ||
|
|
955abcef21 | ||
|
|
9eaf153673 | ||
|
|
8e71c0f2a8 | ||
|
|
18a516ee53 | ||
|
|
f5b2420b4b | ||
|
|
f569561997 | ||
|
|
99c2911a66 | ||
|
|
a0ee6b0d58 | ||
|
|
d891c8c981 | ||
|
|
07e34fbe84 | ||
|
|
7dfd32a5c4 | ||
|
|
9c7f55d8d0 | ||
|
|
e496dc710f | ||
|
|
13cdc44caf | ||
|
|
1f3f9781d9 | ||
|
|
57ddd5086f | ||
|
|
6c1fb1bd02 | ||
|
|
ea92e92c5a | ||
|
|
96eaea7db9 | ||
|
|
bbac6e2ba6 | ||
|
|
76c6fbbfba | ||
|
|
ad1c3d4910 | ||
|
|
f2137683f9 | ||
|
|
084b86cab1 | ||
|
|
41af9c8900 | ||
|
|
2d58cfaa05 | ||
|
|
4af3072b53 | ||
|
|
a37d2ff4f8 | ||
|
|
c525939b13 | ||
|
|
dc186a57cd | ||
|
|
d97dd266b7 | ||
|
|
df9a6a0c53 | ||
|
|
fd38daf0c5 | ||
|
|
28b4f6b8fd | ||
|
|
2db50dd4a7 | ||
|
|
5cd9c11169 | ||
|
|
f8f5d6876b | ||
|
|
198674f368 | ||
|
|
e22eafc4a7 | ||
|
|
f44a322df5 | ||
|
|
fc2ac8a02b | ||
|
|
13243785f1 | ||
|
|
35c207e936 | ||
|
|
998608111f | ||
|
|
6018700421 | ||
|
|
c171547037 | ||
|
|
493b7d594d | ||
|
|
4d40c015e4 | ||
|
|
4405bc4182 | ||
|
|
54a0639a6e | ||
|
|
334b286ebf | ||
|
|
c09cb5df3d | ||
|
|
0da3661ff0 | ||
|
|
5a4ccbc066 | ||
|
|
49cbdc22da | ||
|
|
579ed0a985 | ||
|
|
464797858f | ||
|
|
0ff46bf5d0 | ||
|
|
330abe5a2d | ||
|
|
73945899fe | ||
|
|
8227a9ff9c | ||
|
|
f1c70cd896 | ||
|
|
b68c64041e | ||
|
|
36066068d4 | ||
|
|
8ed174e7af |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
*.pyc
|
||||
configuration.py
|
||||
/netbox/netbox/configuration.py
|
||||
/netbox/static
|
||||
.idea
|
||||
/*.sh
|
||||
!upgrade.sh
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -5,7 +5,7 @@ WORKDIR /opt/netbox
|
||||
ARG BRANCH=master
|
||||
ARG URL=https://github.com/digitalocean/netbox.git
|
||||
RUN git clone --depth 1 $URL -b $BRANCH . && \
|
||||
apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev && \
|
||||
apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev graphviz && \
|
||||
pip install gunicorn==17.5 && \
|
||||
pip install django-auth-ldap && \
|
||||
pip install -r requirements.txt
|
||||
|
||||
@@ -98,4 +98,4 @@ dist-switch\d
|
||||
access-switch\d+,oob-switch\d+
|
||||
```
|
||||
|
||||
Note that you can combine multiple regexes onto one line using commas. (Commas can only be used for separating regexes; they will not be processed as part of a regex.) The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.
|
||||
Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.
|
||||
|
||||
@@ -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:
|
||||
|
||||
```
|
||||
# cp /opt/netbox-X.Y.Z/configuration.py /opt/netbox/configuration.py
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
|
||||
@@ -55,7 +54,10 @@ 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']
|
||||
|
||||
|
||||
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
@@ -86,7 +88,7 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
attrs={'filter-for': 'interface'}))
|
||||
display_field='display_name', attrs={'filter-for': 'interface'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
@@ -178,10 +180,13 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
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']
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
|
||||
@@ -42,7 +42,7 @@ class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Provider
|
||||
form_class = forms.ProviderForm
|
||||
template_name = 'circuits/provider_edit.html'
|
||||
cancel_url = 'circuits:provider_list'
|
||||
obj_list_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
@@ -88,8 +88,8 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuittype'
|
||||
model = CircuitType
|
||||
form_class = forms.CircuitTypeForm
|
||||
success_url = 'circuits:circuittype_list'
|
||||
cancel_url = 'circuits:circuittype_list'
|
||||
obj_list_url = 'circuits:circuittype_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -126,7 +126,7 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
form_class = forms.CircuitForm
|
||||
fields_initial = ['site']
|
||||
template_name = 'circuits/circuit_edit.html'
|
||||
cancel_url = 'circuits:circuit_list'
|
||||
obj_list_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
@@ -183,10 +183,14 @@ class DeviceAdmin(admin.ModelAdmin):
|
||||
DeviceBayAdmin,
|
||||
ModuleAdmin,
|
||||
]
|
||||
list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'asset_tag',
|
||||
list_display = ['display_name', 'device_type_full_name', 'device_role', 'primary_ip', 'rack', 'position', 'asset_tag',
|
||||
'serial']
|
||||
list_filter = ['device_role']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(DeviceAdmin, self).get_queryset(request)
|
||||
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')
|
||||
|
||||
def device_type_full_name(self, obj):
|
||||
return obj.device_type.full_name
|
||||
device_type_full_name.short_description = 'Device type'
|
||||
|
||||
@@ -79,7 +79,7 @@ class RackSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
|
||||
'u_height', 'comments', 'custom_fields']
|
||||
'u_height', 'desc_units', 'comments', 'custom_fields']
|
||||
|
||||
|
||||
class RackNestedSerializer(RackSerializer):
|
||||
@@ -94,7 +94,7 @@ class RackDetailSerializer(RackSerializer):
|
||||
|
||||
class Meta(RackSerializer.Meta):
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
|
||||
'u_height', 'comments', 'custom_fields', 'front_units', 'rear_units']
|
||||
'u_height', 'desc_units', 'comments', 'custom_fields', 'front_units', 'rear_units']
|
||||
|
||||
def get_front_units(self, obj):
|
||||
units = obj.get_rack_units(face=RACK_FACE_FRONT)
|
||||
|
||||
@@ -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,15 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
def _mac_address(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
return queryset.filter(interfaces__mac_address=value).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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, Q
|
||||
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
|
||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||
APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
|
||||
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
||||
SlugField,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||
Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole,
|
||||
Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES,
|
||||
Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||
)
|
||||
|
||||
|
||||
@@ -42,37 +43,12 @@ def get_device_by_name_or_pk(name):
|
||||
return device
|
||||
|
||||
|
||||
def bulkedit_platform_choices():
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(p.pk, p.name) for p in Platform.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
def bulkedit_rackgroup_choices():
|
||||
def validate_connection_status(value):
|
||||
"""
|
||||
Include an option to remove the currently assigned group from a rack.
|
||||
Custom validator for connection statuses. value must be either "planned" or "connected" (case-insensitive).
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(r.pk, r) for r in RackGroup.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
def bulkedit_rackrole_choices():
|
||||
"""
|
||||
Include an option to remove the currently assigned role from a rack.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(r.pk, r.name) for r in RackRole.objects.all()]
|
||||
return choices
|
||||
if value.lower() not in ['planned', 'connected']:
|
||||
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
||||
|
||||
|
||||
#
|
||||
@@ -114,7 +90,10 @@ class SiteImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant']
|
||||
|
||||
|
||||
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
@@ -163,7 +142,8 @@ class RackForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'comments']
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
||||
'comments']
|
||||
help_texts = {
|
||||
'site': "The site at which the rack exists",
|
||||
'name': "Organizational rack name",
|
||||
@@ -199,7 +179,8 @@ class RackFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height']
|
||||
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height',
|
||||
'desc_units']
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -234,13 +215,16 @@ class RackImportForm(BulkImportForm, BootstrapMixin):
|
||||
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
|
||||
group = forms.TypedChoiceField(choices=bulkedit_rackgroup_choices, coerce=int, required=False, label='Group')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
role = forms.TypedChoiceField(choices=bulkedit_rackrole_choices, coerce=int, required=False, label='Role')
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False)
|
||||
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']
|
||||
|
||||
|
||||
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
@@ -279,11 +263,14 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
|
||||
'is_pdu', 'is_network_device', 'subdevice_role']
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
||||
class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||
u_height = forms.IntegerField(min_value=1, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
||||
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||
@@ -334,6 +321,14 @@ class InterfaceTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name_pattern', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class DeviceBayTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
@@ -378,7 +373,7 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
attrs={'filter-for': 'position'}
|
||||
))
|
||||
position = forms.TypedChoiceField(required=False, empty_value=None,
|
||||
help_text="For multi-U devices, this is the lowest occupied rack unit.",
|
||||
help_text="The lowest-numbered unit occupied by the device",
|
||||
widget=APISelect(api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
|
||||
disabled_indicator='device'))
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
|
||||
@@ -583,12 +578,26 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False,
|
||||
label='Platform')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
|
||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'platform']
|
||||
|
||||
|
||||
class DeviceBulkAddComponentForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name_pattern', 'form_factor', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Device
|
||||
@@ -603,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(required=False, label='MAC address')
|
||||
|
||||
|
||||
#
|
||||
@@ -631,7 +641,7 @@ class ConsoleConnectionCSVForm(forms.Form):
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
console_port = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
status = forms.CharField(validators=[validate_connection_status])
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -695,6 +705,7 @@ class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
||||
console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'cs_port'}))
|
||||
livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
|
||||
@@ -762,7 +773,7 @@ class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
attrs={'filter-for': 'port'}))
|
||||
display_field='display_name', attrs={'filter-for': 'port'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
@@ -826,7 +837,7 @@ class PowerConnectionCSVForm(forms.Form):
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
power_port = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
status = forms.CharField(validators=[validate_connection_status])
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -891,7 +902,7 @@ class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
||||
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
|
||||
attrs={'filter-for': 'power_outlet'}))
|
||||
display_field='display_name', attrs={'filter-for': 'power_outlet'}))
|
||||
livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
|
||||
)
|
||||
@@ -958,7 +969,7 @@ class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
attrs={'filter-for': 'port'}))
|
||||
display_field='display_name', attrs={'filter-for': 'port'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
@@ -1019,8 +1030,13 @@ class InterfaceCreateForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
#
|
||||
@@ -1033,6 +1049,7 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'device_b'}))
|
||||
device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface_b'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
|
||||
@@ -1087,7 +1104,7 @@ class InterfaceConnectionCSVForm(forms.Form):
|
||||
device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device B not found.'})
|
||||
interface_b = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
status = forms.CharField(validators=[validate_connection_status])
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -1223,15 +1240,12 @@ class InterfaceConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
||||
class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'interface', 'set_as_primary']
|
||||
help_texts = {
|
||||
'address': 'IPv4 or IPv6 address (with mask)'
|
||||
}
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'description']
|
||||
|
||||
def __init__(self, device, *args, **kwargs):
|
||||
|
||||
@@ -1239,16 +1253,21 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
self.fields['interface'].queryset = device.interfaces.all()
|
||||
interfaces = device.interfaces.all()
|
||||
self.fields['interface'].queryset = interfaces
|
||||
self.fields['interface'].required = True
|
||||
|
||||
# If this device does not have any IP addresses assigned, default to setting the first IP as its primary
|
||||
# If this device has only one interface, select it by default.
|
||||
if len(interfaces) == 1:
|
||||
self.fields['interface'].initial = interfaces[0]
|
||||
|
||||
# If this device does not have any IP addresses assigned, default to setting the first IP as its primary.
|
||||
if not IPAddress.objects.filter(interface__device=device).count():
|
||||
self.fields['set_as_primary'].initial = True
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
# Modules
|
||||
#
|
||||
|
||||
class ModuleForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
20
netbox/dcim/migrations/0020_rack_desc_units.py
Normal file
20
netbox/dcim/migrations/0020_rack_desc_units.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-10-28 15:01
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0019_new_iface_form_factors'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='desc_units',
|
||||
field=models.BooleanField(default=False, help_text=b'Units are numbered top-to-bottom', verbose_name=b'Descending units'),
|
||||
),
|
||||
]
|
||||
31
netbox/dcim/migrations/0021_add_ff_flexstack.py
Normal file
31
netbox/dcim/migrations/0021_add_ff_flexstack.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-10-31 18:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0020_rack_desc_units'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, help_text=b'The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
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
|
||||
@@ -107,6 +84,8 @@ IFACE_FF_E3 = 4050
|
||||
# Stacking
|
||||
IFACE_FF_STACKWISE = 5000
|
||||
IFACE_FF_STACKWISE_PLUS = 5050
|
||||
IFACE_FF_FLEXSTACK = 5100
|
||||
IFACE_FF_FLEXSTACK_PLUS = 5150
|
||||
# Other
|
||||
IFACE_FF_OTHER = 32767
|
||||
|
||||
@@ -164,6 +143,8 @@ IFACE_FF_CHOICES = [
|
||||
[
|
||||
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
|
||||
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
||||
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -341,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']
|
||||
@@ -375,6 +356,8 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
help_text='Rail-to-rail width')
|
||||
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
desc_units = models.BooleanField(default=False, verbose_name='Descending units',
|
||||
help_text='Units are numbered top-to-bottom')
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
@@ -401,8 +384,11 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
if top_device:
|
||||
min_height = top_device.position + top_device.device_type.u_height - 1
|
||||
if self.u_height < min_height:
|
||||
raise ValidationError("Rack must be at least {}U tall with currently installed devices."
|
||||
.format(min_height))
|
||||
raise ValidationError({
|
||||
'u_height': "Rack must be at least {}U tall to house currently installed devices.".format(
|
||||
min_height
|
||||
)
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
@@ -419,7 +405,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return reversed(range(1, self.u_height + 1))
|
||||
if self.desc_units:
|
||||
return range(1, self.u_height + 1)
|
||||
else:
|
||||
return reversed(range(1, self.u_height + 1))
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
@@ -438,7 +427,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
|
||||
elevation = OrderedDict()
|
||||
for u in reversed(range(1, self.u_height + 1)):
|
||||
for u in self.units:
|
||||
elevation[u] = {'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None}
|
||||
|
||||
# Add devices to rack units list
|
||||
@@ -476,7 +465,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
|
||||
# Gather all devices which consume U space within the rack
|
||||
devices = self.devices.select_related().filter(position__gte=1).exclude(pk__in=exclude)
|
||||
devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
|
||||
|
||||
# Initialize the rack unit skeleton
|
||||
units = range(1, self.u_height + 1)
|
||||
@@ -506,9 +495,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
Determine the utilization rate of the rack and return it as a percentage.
|
||||
"""
|
||||
if self.u_consumed is None:
|
||||
self.u_consumed = 0
|
||||
u_available = self.u_height - self.u_consumed
|
||||
u_available = len(self.get_available_units())
|
||||
return int(float(self.u_height - u_available) / self.u_height * 100)
|
||||
|
||||
|
||||
@@ -574,7 +561,7 @@ class DeviceType(models.Model):
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{} {}'.format(self.manufacturer, self.model)
|
||||
return self.model
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DeviceType, self).__init__(*args, **kwargs)
|
||||
@@ -596,27 +583,43 @@ class DeviceType(models.Model):
|
||||
u_available = d.rack.get_available_units(u_height=self.u_height, rack_face=face_required,
|
||||
exclude=[d.pk])
|
||||
if d.position not in u_available:
|
||||
raise ValidationError("Device {} in rack {} does not have sufficient space to accommodate a height "
|
||||
"of {}U".format(d, d.rack, self.u_height))
|
||||
raise ValidationError({
|
||||
'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of "
|
||||
"{}U".format(d, d.rack, self.u_height)
|
||||
})
|
||||
|
||||
if not self.is_console_server and self.cs_port_templates.count():
|
||||
raise ValidationError("Must delete all console server port templates associated with this device before "
|
||||
"declassifying it as a console server.")
|
||||
raise ValidationError({
|
||||
'is_console_server': "Must delete all console server port templates associated with this device before "
|
||||
"declassifying it as a console server."
|
||||
})
|
||||
|
||||
if not self.is_pdu and self.power_outlet_templates.count():
|
||||
raise ValidationError("Must delete all power outlet templates associated with this device before "
|
||||
"declassifying it as a PDU.")
|
||||
raise ValidationError({
|
||||
'is_pdu': "Must delete all power outlet templates associated with this device before declassifying it "
|
||||
"as a PDU."
|
||||
})
|
||||
|
||||
if not self.is_network_device and self.interface_templates.filter(mgmt_only=False).count():
|
||||
raise ValidationError("Must delete all non-management-only interface templates associated with this device "
|
||||
"before declassifying it as a network device.")
|
||||
raise ValidationError({
|
||||
'is_network_device': "Must delete all non-management-only interface templates associated with this "
|
||||
"device before declassifying it as a network device."
|
||||
})
|
||||
|
||||
if self.subdevice_role != SUBDEVICE_ROLE_PARENT and self.device_bay_templates.count():
|
||||
raise ValidationError("Must delete all device bay templates associated with this device before "
|
||||
"declassifying it as a parent device.")
|
||||
raise ValidationError({
|
||||
'subdevice_role': "Must delete all device bay templates associated with this device before "
|
||||
"declassifying it as a parent device."
|
||||
})
|
||||
|
||||
if self.u_height and self.subdevice_role == SUBDEVICE_ROLE_CHILD:
|
||||
raise ValidationError("Child device types must be 0U.")
|
||||
raise ValidationError({
|
||||
'u_height': "Child device types must be 0U."
|
||||
})
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return u'{} {}'.format(self.manufacturer.name, self.model)
|
||||
|
||||
@property
|
||||
def is_parent_device(self):
|
||||
@@ -739,7 +742,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']
|
||||
@@ -800,7 +803,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
rack = models.ForeignKey('Rack', related_name='devices', on_delete=models.PROTECT)
|
||||
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
|
||||
verbose_name='Position (U)',
|
||||
help_text='Number of the lowest U position occupied by the device')
|
||||
help_text='The lowest-numbered unit occupied by the device')
|
||||
face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
|
||||
status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
|
||||
primary_ip4 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL,
|
||||
@@ -824,28 +827,40 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate device type assignment
|
||||
if not hasattr(self, 'device_type'):
|
||||
raise ValidationError("Must specify device type.")
|
||||
|
||||
# Child devices cannot be assigned to a rack face/unit
|
||||
if self.device_type.is_child_device and (self.face is not None or self.position):
|
||||
raise ValidationError("Child device types cannot be assigned a rack face or position.")
|
||||
|
||||
# Validate position/face combination
|
||||
if self.position and self.face is None:
|
||||
raise ValidationError("Must specify rack face with rack position.")
|
||||
raise ValidationError({
|
||||
'face': "Must specify rack face when defining rack position."
|
||||
})
|
||||
|
||||
# Validate rack space
|
||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||
exclude_list = [self.pk] if self.pk else []
|
||||
try:
|
||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||
exclude=exclude_list)
|
||||
if self.position and self.position not in available_units:
|
||||
raise ValidationError("U{} is already occupied or does not have sufficient space to accommodate a(n) "
|
||||
"{} ({}U).".format(self.position, self.device_type, self.device_type.u_height))
|
||||
except Rack.DoesNotExist:
|
||||
# Child devices cannot be assigned to a rack face/unit
|
||||
if self.device_type.is_child_device and self.face is not None:
|
||||
raise ValidationError({
|
||||
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
|
||||
"device."
|
||||
})
|
||||
if self.device_type.is_child_device and self.position:
|
||||
raise ValidationError({
|
||||
'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
|
||||
"parent device."
|
||||
})
|
||||
|
||||
# Validate rack space
|
||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||
exclude_list = [self.pk] if self.pk else []
|
||||
try:
|
||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||
exclude=exclude_list)
|
||||
if self.position and self.position not in available_units:
|
||||
raise ValidationError({
|
||||
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
|
||||
"({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
||||
})
|
||||
except Rack.DoesNotExist:
|
||||
pass
|
||||
|
||||
except DeviceType.DoesNotExist:
|
||||
pass
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -961,6 +976,9 @@ class ConsolePort(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
@@ -1002,6 +1020,9 @@ class ConsoleServerPort(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
|
||||
class PowerPort(models.Model):
|
||||
"""
|
||||
@@ -1020,6 +1041,9 @@ class PowerPort(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
@@ -1055,6 +1079,9 @@ class PowerOutlet(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
|
||||
class InterfaceManager(models.Manager):
|
||||
|
||||
@@ -1091,12 +1118,16 @@ class Interface(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected:
|
||||
raise ValidationError({'form_factor': "Virtual interfaces cannot be connected to another interface or "
|
||||
"circuit. Disconnect the interface or choose a physical form "
|
||||
"factor."})
|
||||
raise ValidationError({
|
||||
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
||||
"interface or choose a physical form factor."
|
||||
})
|
||||
|
||||
@property
|
||||
def is_physical(self):
|
||||
@@ -1123,16 +1154,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):
|
||||
@@ -1147,7 +1175,9 @@ class InterfaceConnection(models.Model):
|
||||
|
||||
def clean(self):
|
||||
if self.interface_a == self.interface_b:
|
||||
raise ValidationError("Cannot connect an interface to itself")
|
||||
raise ValidationError({
|
||||
'interface_b': "Cannot connect an interface to itself."
|
||||
})
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
@@ -1176,12 +1206,16 @@ class DeviceBay(models.Model):
|
||||
def __unicode__(self):
|
||||
return u'{} - {}'.format(self.device.name, self.name)
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that the parent Device can have DeviceBays
|
||||
if not self.device.device_type.is_parent_device:
|
||||
raise ValidationError("This type of device ({}) does not support device bays."
|
||||
.format(self.device.device_type))
|
||||
raise ValidationError("This type of device ({}) does not support device bays.".format(
|
||||
self.device.device_type
|
||||
))
|
||||
|
||||
# Cannot install a device into itself, obviously
|
||||
if self.device == self.installed_device:
|
||||
@@ -1208,3 +1242,6 @@ class Module(models.Model):
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_parent_url(self):
|
||||
return reverse('dcim:device_inventory', args=[self.device.pk])
|
||||
|
||||
@@ -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 = """
|
||||
@@ -72,7 +72,7 @@ STATUS_ICON = """
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph record.get_utilization %}
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
|
||||
@@ -148,13 +148,12 @@ class RackTable(BaseTable):
|
||||
role = tables.TemplateColumn(RACK_ROLE, verbose_name='Role')
|
||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
|
||||
u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'u_consumed',
|
||||
'utilization')
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices',
|
||||
'get_utilization')
|
||||
|
||||
|
||||
class RackImportTable(BaseTable):
|
||||
@@ -196,10 +195,12 @@ class DeviceTypeTable(BaseTable):
|
||||
manufacturer = tables.Column(verbose_name='Manufacturer')
|
||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||
part_number = tables.Column(verbose_name='Part Number')
|
||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
||||
instance_count = tables.Column(verbose_name='Instances')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height')
|
||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count')
|
||||
|
||||
|
||||
#
|
||||
@@ -293,7 +294,8 @@ class PlatformTable(BaseTable):
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
device_count = tables.Column(verbose_name='Devices')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
@@ -312,7 +314,8 @@ class DeviceTable(BaseTable):
|
||||
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||
device_type = tables.Column(verbose_name='Type')
|
||||
device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name)
|
||||
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
|
||||
template_code="{{ record.primary_ip.address.ip }}")
|
||||
|
||||
@@ -357,7 +360,7 @@ class PowerConnectionTable(BaseTable):
|
||||
args=[Accessor('power_outlet.device.pk')], verbose_name='PDU')
|
||||
power_outlet = tables.Column(verbose_name='Outlet')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
name = tables.Column(verbose_name='Console port')
|
||||
name = tables.Column(verbose_name='Power Port')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
|
||||
@@ -49,6 +49,7 @@ class SiteTest(APITestCase):
|
||||
'type',
|
||||
'width',
|
||||
'u_height',
|
||||
'desc_units',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
]
|
||||
@@ -129,6 +130,7 @@ class RackTest(APITestCase):
|
||||
'type',
|
||||
'width',
|
||||
'u_height',
|
||||
'desc_units',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
]
|
||||
@@ -145,6 +147,7 @@ class RackTest(APITestCase):
|
||||
'type',
|
||||
'width',
|
||||
'u_height',
|
||||
'desc_units',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
'front_units',
|
||||
|
||||
@@ -3,10 +3,6 @@ from django.conf.urls import url
|
||||
from secrets.views import secret_add
|
||||
|
||||
from . import views
|
||||
from .models import (
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, PowerPortTemplate, PowerOutletTemplate,
|
||||
InterfaceTemplate,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -75,6 +71,7 @@ urlpatterns = [
|
||||
|
||||
# Interface templates
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||
|
||||
# Device bay templates
|
||||
@@ -113,38 +110,38 @@ urlpatterns = [
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.consoleport_edit, name='consoleport_edit'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.consoleport_delete, name='consoleport_delete'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
|
||||
# Console server ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.consoleserverport_edit, name='consoleserverport_edit'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.consoleserverport_delete, name='consoleserverport_delete'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
|
||||
# Power ports
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, name='powerport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.powerport_edit, name='powerport_edit'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.powerport_delete, name='powerport_delete'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
|
||||
# Power outlets
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.poweroutlet_delete, name='poweroutlet_delete'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
|
||||
# Device bays
|
||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.devicebay_edit, name='devicebay_edit'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.devicebay_delete, name='devicebay_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
|
||||
|
||||
@@ -157,17 +154,18 @@ urlpatterns = [
|
||||
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_add_multi'),
|
||||
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.interface_edit, name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.interface_delete, name='interface_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
|
||||
# Modules
|
||||
url(r'^devices/(?P<pk>\d+)/modules/add/$', views.module_add, name='module_add'),
|
||||
url(r'^modules/(?P<pk>\d+)/edit/$', views.module_edit, name='module_edit'),
|
||||
url(r'^modules/(?P<pk>\d+)/delete/$', views.module_delete, name='module_delete'),
|
||||
url(r'^modules/(?P<pk>\d+)/edit/$', views.ModuleEditView.as_view(), name='module_edit'),
|
||||
url(r'^modules/(?P<pk>\d+)/delete/$', views.ModuleDeleteView.as_view(), name='module_delete'),
|
||||
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from copy import deepcopy
|
||||
import re
|
||||
from natsort import natsorted
|
||||
from operator import attrgetter
|
||||
@@ -7,8 +8,7 @@ from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models import Count
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.http import urlencode
|
||||
@@ -98,7 +98,7 @@ class SiteEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Site
|
||||
form_class = forms.SiteForm
|
||||
template_name = 'dcim/site_edit.html'
|
||||
cancel_url = 'dcim:site_list'
|
||||
obj_list_url = 'dcim:site_list'
|
||||
|
||||
|
||||
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
@@ -140,8 +140,8 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_rackgroup'
|
||||
model = RackGroup
|
||||
form_class = forms.RackGroupForm
|
||||
success_url = 'dcim:rackgroup_list'
|
||||
cancel_url = 'dcim:rackgroup_list'
|
||||
obj_list_url = 'dcim:rackgroup_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -165,8 +165,8 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_rackrole'
|
||||
model = RackRole
|
||||
form_class = forms.RackRoleForm
|
||||
success_url = 'dcim:rackrole_list'
|
||||
cancel_url = 'dcim:rackrole_list'
|
||||
obj_list_url = 'dcim:rackrole_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -181,8 +181,7 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class RackListView(ObjectListView):
|
||||
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\
|
||||
.annotate(device_count=Count('devices', distinct=True),
|
||||
u_consumed=Coalesce(Sum('devices__device_type__u_height'), 0))
|
||||
.annotate(device_count=Count('devices', distinct=True))
|
||||
filter = filters.RackFilter
|
||||
filter_form = forms.RackFilterForm
|
||||
table = tables.RackTable
|
||||
@@ -214,7 +213,7 @@ class RackEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Rack
|
||||
form_class = forms.RackForm
|
||||
template_name = 'dcim/rack_edit.html'
|
||||
cancel_url = 'dcim:rack_list'
|
||||
obj_list_url = 'dcim:rack_list'
|
||||
|
||||
|
||||
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
@@ -260,8 +259,8 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_manufacturer'
|
||||
model = Manufacturer
|
||||
form_class = forms.ManufacturerForm
|
||||
success_url = 'dcim:manufacturer_list'
|
||||
cancel_url = 'dcim:manufacturer_list'
|
||||
obj_list_url = 'dcim:manufacturer_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -275,7 +274,7 @@ class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class DeviceTypeListView(ObjectListView):
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
|
||||
filter = filters.DeviceTypeFilter
|
||||
filter_form = forms.DeviceTypeFilterForm
|
||||
table = tables.DeviceTypeTable
|
||||
@@ -332,7 +331,7 @@ class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_devicetype'
|
||||
model = DeviceType
|
||||
form_class = forms.DeviceTypeForm
|
||||
cancel_url = 'dcim:devicetype_list'
|
||||
obj_list_url = 'dcim:devicetype_list'
|
||||
|
||||
|
||||
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
@@ -394,7 +393,7 @@ class ComponentTemplateCreateView(View):
|
||||
|
||||
if not form.errors:
|
||||
self.model.objects.bulk_create(component_templates)
|
||||
messages.success(request, "Added {} component(s) to {}".format(len(component_templates), devicetype))
|
||||
messages.success(request, u"Added {} component(s) to {}.".format(len(component_templates), devicetype))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
else:
|
||||
@@ -457,6 +456,14 @@ class InterfaceTemplateAddView(ComponentTemplateCreateView):
|
||||
form = forms.InterfaceTemplateForm
|
||||
|
||||
|
||||
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interfacetemplate'
|
||||
cls = InterfaceTemplate
|
||||
parent_cls = DeviceType
|
||||
form = forms.InterfaceTemplateBulkEditForm
|
||||
template_name = 'dcim/interfacetemplate_bulk_edit.html'
|
||||
|
||||
|
||||
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interfacetemplate'
|
||||
cls = InterfaceTemplate
|
||||
@@ -489,8 +496,8 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_devicerole'
|
||||
model = DeviceRole
|
||||
form_class = forms.DeviceRoleForm
|
||||
success_url = 'dcim:devicerole_list'
|
||||
cancel_url = 'dcim:devicerole_list'
|
||||
obj_list_url = 'dcim:devicerole_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -514,8 +521,8 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_platform'
|
||||
model = Platform
|
||||
form_class = forms.PlatformForm
|
||||
success_url = 'dcim:platform_list'
|
||||
cancel_url = 'dcim:platform_list'
|
||||
obj_list_url = 'dcim:platform_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -566,7 +573,8 @@ def device(request, pk):
|
||||
secrets = device.secrets.all()
|
||||
|
||||
# Find all IP addresses assigned to this device
|
||||
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface').order_by('address')
|
||||
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\
|
||||
.order_by('address')
|
||||
|
||||
# Find any related devices for convenient linking in the UI
|
||||
related_devices = []
|
||||
@@ -608,7 +616,7 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
form_class = forms.DeviceForm
|
||||
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
|
||||
template_name = 'dcim/device_edit.html'
|
||||
cancel_url = 'dcim:device_list'
|
||||
obj_list_url = 'dcim:device_list'
|
||||
|
||||
|
||||
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
@@ -679,6 +687,80 @@ def device_lldp_neighbors(request, pk):
|
||||
})
|
||||
|
||||
|
||||
class DeviceBulkAddComponentView(View):
|
||||
"""
|
||||
Add one or more components (e.g. interfaces) to a selected set of Devices.
|
||||
"""
|
||||
form = None
|
||||
component_cls = None
|
||||
component_form = None
|
||||
|
||||
def get(self):
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
def post(self, request):
|
||||
|
||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all'):
|
||||
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
if '_create' in request.POST:
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
new_components = []
|
||||
data = deepcopy(form.cleaned_data)
|
||||
for device in data['pk']:
|
||||
|
||||
names = data['name_pattern']
|
||||
for name in names:
|
||||
component_data = {
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
}
|
||||
component_data.update(data)
|
||||
component_form = self.component_form(component_data)
|
||||
if component_form.is_valid():
|
||||
new_components.append(component_form.save(commit=False))
|
||||
else:
|
||||
form.add_error('name_pattern', "Duplicate {} name for {}: {}".format(
|
||||
self.component_cls._meta.verbose_name, device, name
|
||||
))
|
||||
|
||||
if not form.errors:
|
||||
self.component_cls.objects.bulk_create(new_components)
|
||||
messages.success(request, u"Added {} {} to {} devices.".format(
|
||||
len(new_components), self.component_cls._meta.verbose_name_plural, len(form.cleaned_data['pk'])
|
||||
))
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
else:
|
||||
form = self.form(initial={'pk': pk_list})
|
||||
|
||||
selected_devices = Device.objects.filter(pk__in=pk_list)
|
||||
if not selected_devices:
|
||||
messages.warning(request, u"No devices were selected.")
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
return render(request, 'dcim/device_bulk_add_component.html', {
|
||||
'form': form,
|
||||
'component_name': self.component_cls._meta.verbose_name_plural,
|
||||
'selected_devices': selected_devices,
|
||||
'cancel_url': reverse('dcim:device_list'),
|
||||
})
|
||||
|
||||
|
||||
class DeviceBulkAddInterfaceView(DeviceBulkAddComponentView):
|
||||
"""
|
||||
Add one or more components (e.g. interfaces) to a selected set of Devices.
|
||||
"""
|
||||
form = forms.DeviceBulkAddInterfaceForm
|
||||
component_cls = Interface
|
||||
component_form = forms.InterfaceForm
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
@@ -705,7 +787,7 @@ def consoleport_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
ConsolePort.objects.bulk_create(console_ports)
|
||||
messages.success(request, "Added {} console port(s) to {}".format(len(console_ports), device))
|
||||
messages.success(request, u"Added {} console port(s) to {}.".format(len(console_ports), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:consoleport_add', pk=device.pk)
|
||||
else:
|
||||
@@ -714,8 +796,9 @@ def consoleport_add(request, pk):
|
||||
else:
|
||||
form = forms.ConsolePortCreateForm()
|
||||
|
||||
return render(request, 'dcim/consoleport_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Console Port',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
@@ -730,7 +813,7 @@ def consoleport_connect(request, pk):
|
||||
form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
|
||||
if form.is_valid():
|
||||
consoleport = form.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
consoleport.device,
|
||||
consoleport.name,
|
||||
consoleport.cs_port.device,
|
||||
@@ -757,7 +840,7 @@ def consoleport_disconnect(request, pk):
|
||||
consoleport = get_object_or_404(ConsolePort, pk=pk)
|
||||
|
||||
if not consoleport.cs_port:
|
||||
messages.warning(request, "Cannot disconnect console port {0}: It is not connected to anything"
|
||||
messages.warning(request, u"Cannot disconnect console port {}: It is not connected to anything."
|
||||
.format(consoleport))
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
@@ -767,7 +850,7 @@ def consoleport_disconnect(request, pk):
|
||||
consoleport.cs_port = None
|
||||
consoleport.connection_status = None
|
||||
consoleport.save()
|
||||
messages.success(request, "Console port {0} has been disconnected".format(consoleport))
|
||||
messages.success(request, u"Console port {} has been disconnected.".format(consoleport))
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -780,49 +863,15 @@ def consoleport_disconnect(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_consoleport')
|
||||
def consoleport_edit(request, pk):
|
||||
|
||||
consoleport = get_object_or_404(ConsolePort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.ConsolePortForm(request.POST, instance=consoleport)
|
||||
if form.is_valid():
|
||||
consoleport = form.save()
|
||||
messages.success(request, "Modified {0} {1}".format(consoleport.device.name, consoleport.name))
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.ConsolePortForm(instance=consoleport)
|
||||
|
||||
return render(request, 'dcim/consoleport_edit.html', {
|
||||
'consoleport': consoleport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
|
||||
})
|
||||
class ConsolePortEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_consoleport'
|
||||
model = ConsolePort
|
||||
form_class = forms.ConsolePortForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_consoleport')
|
||||
def consoleport_delete(request, pk):
|
||||
|
||||
consoleport = get_object_or_404(ConsolePort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
consoleport.delete()
|
||||
messages.success(request, "Console port {0} has been deleted from {1}".format(consoleport,
|
||||
consoleport.device))
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/consoleport_delete.html', {
|
||||
'consoleport': consoleport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
|
||||
})
|
||||
class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_consoleport'
|
||||
model = ConsolePort
|
||||
|
||||
|
||||
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -865,7 +914,7 @@ def consoleserverport_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
ConsoleServerPort.objects.bulk_create(cs_ports)
|
||||
messages.success(request, "Added {} console server port(s) to {}".format(len(cs_ports), device))
|
||||
messages.success(request, u"Added {} console server port(s) to {}.".format(len(cs_ports), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:consoleserverport_add', pk=device.pk)
|
||||
else:
|
||||
@@ -874,8 +923,9 @@ def consoleserverport_add(request, pk):
|
||||
else:
|
||||
form = forms.ConsoleServerPortCreateForm()
|
||||
|
||||
return render(request, 'dcim/consoleserverport_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Console Server Port',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
@@ -893,7 +943,7 @@ def consoleserverport_connect(request, pk):
|
||||
consoleport.cs_port = consoleserverport
|
||||
consoleport.connection_status = form.cleaned_data['connection_status']
|
||||
consoleport.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
consoleport.device,
|
||||
consoleport.name,
|
||||
consoleserverport.device,
|
||||
@@ -917,7 +967,7 @@ def consoleserverport_disconnect(request, pk):
|
||||
consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
|
||||
|
||||
if not hasattr(consoleserverport, 'connected_console'):
|
||||
messages.warning(request, "Cannot disconnect console server port {0}: Nothing is connected to it"
|
||||
messages.warning(request, u"Cannot disconnect console server port {}: Nothing is connected to it."
|
||||
.format(consoleserverport))
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
@@ -928,7 +978,7 @@ def consoleserverport_disconnect(request, pk):
|
||||
consoleport.cs_port = None
|
||||
consoleport.connection_status = None
|
||||
consoleport.save()
|
||||
messages.success(request, "Console server port {0} has been disconnected".format(consoleserverport))
|
||||
messages.success(request, u"Console server port {} has been disconnected.".format(consoleserverport))
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -941,49 +991,15 @@ def consoleserverport_disconnect(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_consoleserverport')
|
||||
def consoleserverport_edit(request, pk):
|
||||
|
||||
consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.ConsoleServerPortForm(request.POST, instance=consoleserverport)
|
||||
if form.is_valid():
|
||||
consoleserverport = form.save()
|
||||
messages.success(request, "Modified {0} {1}".format(consoleserverport.device.name, consoleserverport.name))
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.ConsoleServerPortForm(instance=consoleserverport)
|
||||
|
||||
return render(request, 'dcim/consoleserverport_edit.html', {
|
||||
'consoleserverport': consoleserverport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
|
||||
})
|
||||
class ConsoleServerPortEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_consoleserverport'
|
||||
model = ConsoleServerPort
|
||||
form_class = forms.ConsoleServerPortForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_consoleserverport')
|
||||
def consoleserverport_delete(request, pk):
|
||||
|
||||
consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
consoleserverport.delete()
|
||||
messages.success(request, "Console server port {0} has been deleted from {1}"
|
||||
.format(consoleserverport, consoleserverport.device))
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/consoleserverport_delete.html', {
|
||||
'consoleserverport': consoleserverport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
|
||||
})
|
||||
class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverport'
|
||||
model = ConsoleServerPort
|
||||
|
||||
|
||||
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -1018,7 +1034,7 @@ def powerport_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
PowerPort.objects.bulk_create(power_ports)
|
||||
messages.success(request, "Added {} power port(s) to {}".format(len(power_ports), device))
|
||||
messages.success(request, u"Added {} power port(s) to {}.".format(len(power_ports), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:powerport_add', pk=device.pk)
|
||||
else:
|
||||
@@ -1027,8 +1043,9 @@ def powerport_add(request, pk):
|
||||
else:
|
||||
form = forms.PowerPortCreateForm()
|
||||
|
||||
return render(request, 'dcim/powerport_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Power Port',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
@@ -1043,7 +1060,7 @@ def powerport_connect(request, pk):
|
||||
form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
|
||||
if form.is_valid():
|
||||
powerport = form.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
powerport.device,
|
||||
powerport.name,
|
||||
powerport.power_outlet.device,
|
||||
@@ -1070,7 +1087,7 @@ def powerport_disconnect(request, pk):
|
||||
powerport = get_object_or_404(PowerPort, pk=pk)
|
||||
|
||||
if not powerport.power_outlet:
|
||||
messages.warning(request, "Cannot disconnect power port {0}: It is not connected to an outlet"
|
||||
messages.warning(request, u"Cannot disconnect power port {}: It is not connected to an outlet."
|
||||
.format(powerport))
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
@@ -1080,7 +1097,7 @@ def powerport_disconnect(request, pk):
|
||||
powerport.power_outlet = None
|
||||
powerport.connection_status = None
|
||||
powerport.save()
|
||||
messages.success(request, "Power port {0} has been disconnected".format(powerport))
|
||||
messages.success(request, u"Power port {} has been disconnected.".format(powerport))
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1093,48 +1110,15 @@ def powerport_disconnect(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_powerport')
|
||||
def powerport_edit(request, pk):
|
||||
|
||||
powerport = get_object_or_404(PowerPort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.PowerPortForm(request.POST, instance=powerport)
|
||||
if form.is_valid():
|
||||
powerport = form.save()
|
||||
messages.success(request, "Modified {0} power port {1}".format(powerport.device.name, powerport.name))
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.PowerPortForm(instance=powerport)
|
||||
|
||||
return render(request, 'dcim/powerport_edit.html', {
|
||||
'powerport': powerport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
|
||||
})
|
||||
class PowerPortEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_powerport'
|
||||
model = PowerPort
|
||||
form_class = forms.PowerPortForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_powerport')
|
||||
def powerport_delete(request, pk):
|
||||
|
||||
powerport = get_object_or_404(PowerPort, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
powerport.delete()
|
||||
messages.success(request, "Power port {0} has been deleted from {1}".format(powerport, powerport.device))
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/powerport_delete.html', {
|
||||
'powerport': powerport,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
|
||||
})
|
||||
class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_powerport'
|
||||
model = PowerPort
|
||||
|
||||
|
||||
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -1176,7 +1160,7 @@ def poweroutlet_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
PowerOutlet.objects.bulk_create(power_outlets)
|
||||
messages.success(request, "Added {} power outlet(s) to {}".format(len(power_outlets), device))
|
||||
messages.success(request, u"Added {} power outlet(s) to {}.".format(len(power_outlets), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:poweroutlet_add', pk=device.pk)
|
||||
else:
|
||||
@@ -1185,8 +1169,9 @@ def poweroutlet_add(request, pk):
|
||||
else:
|
||||
form = forms.PowerOutletCreateForm()
|
||||
|
||||
return render(request, 'dcim/poweroutlet_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Power Outlet',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
@@ -1204,7 +1189,7 @@ def poweroutlet_connect(request, pk):
|
||||
powerport.power_outlet = poweroutlet
|
||||
powerport.connection_status = form.cleaned_data['connection_status']
|
||||
powerport.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
powerport.device,
|
||||
powerport.name,
|
||||
poweroutlet.device,
|
||||
@@ -1228,7 +1213,7 @@ def poweroutlet_disconnect(request, pk):
|
||||
poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
|
||||
|
||||
if not hasattr(poweroutlet, 'connected_port'):
|
||||
messages.warning(request, "Cannot disconnect power outlet {0}: Nothing is connected to it".format(poweroutlet))
|
||||
messages.warning(request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet))
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
@@ -1238,7 +1223,7 @@ def poweroutlet_disconnect(request, pk):
|
||||
powerport.power_outlet = None
|
||||
powerport.connection_status = None
|
||||
powerport.save()
|
||||
messages.success(request, "Power outlet {0} has been disconnected".format(poweroutlet))
|
||||
messages.success(request, u"Power outlet {} has been disconnected.".format(poweroutlet))
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1251,49 +1236,15 @@ def poweroutlet_disconnect(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_poweroutlet')
|
||||
def poweroutlet_edit(request, pk):
|
||||
|
||||
poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.PowerOutletForm(request.POST, instance=poweroutlet)
|
||||
if form.is_valid():
|
||||
poweroutlet = form.save()
|
||||
messages.success(request, "Modified {0} power outlet {1}".format(poweroutlet.device.name, poweroutlet.name))
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.PowerOutletForm(instance=poweroutlet)
|
||||
|
||||
return render(request, 'dcim/poweroutlet_edit.html', {
|
||||
'poweroutlet': poweroutlet,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
|
||||
})
|
||||
class PowerOutletEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_poweroutlet'
|
||||
model = PowerOutlet
|
||||
form_class = forms.PowerOutletForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_poweroutlet')
|
||||
def poweroutlet_delete(request, pk):
|
||||
|
||||
poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
poweroutlet.delete()
|
||||
messages.success(request, "Power outlet {0} has been deleted from {1}".format(poweroutlet,
|
||||
poweroutlet.device))
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/poweroutlet_delete.html', {
|
||||
'poweroutlet': poweroutlet,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
|
||||
})
|
||||
class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlet'
|
||||
model = PowerOutlet
|
||||
|
||||
|
||||
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -1332,97 +1283,40 @@ def interface_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
messages.success(request, "Added {} interface(s) to {}".format(len(interfaces), device))
|
||||
messages.success(request, u"Added {} interface(s) to {}.".format(len(interfaces), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:interface_add', pk=device.pk)
|
||||
else:
|
||||
return redirect('dcim:device', pk=device.pk)
|
||||
|
||||
else:
|
||||
form = forms.InterfaceCreateForm()
|
||||
form = forms.InterfaceCreateForm(initial={'mgmt_only': request.GET.get('mgmt_only')})
|
||||
|
||||
return render(request, 'dcim/interface_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Interface',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_interface')
|
||||
def interface_edit(request, pk):
|
||||
|
||||
interface = get_object_or_404(Interface, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.InterfaceForm(request.POST, instance=interface)
|
||||
if form.is_valid():
|
||||
interface = form.save()
|
||||
messages.success(request, "Modified {0} interface {1}".format(interface.device.name, interface.name))
|
||||
return redirect('dcim:device', pk=interface.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.InterfaceForm(instance=interface)
|
||||
|
||||
return render(request, 'dcim/interface_edit.html', {
|
||||
'interface': interface,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': interface.device.pk}),
|
||||
})
|
||||
class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
model = Interface
|
||||
form_class = forms.InterfaceForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_interface')
|
||||
def interface_delete(request, pk):
|
||||
|
||||
interface = get_object_or_404(Interface, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
interface.delete()
|
||||
messages.success(request, "Interface {0} has been deleted from {1}".format(interface, interface.device))
|
||||
return redirect('dcim:device', pk=interface.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/interface_delete.html', {
|
||||
'interface': interface,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': interface.device.pk}),
|
||||
})
|
||||
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
model = Interface
|
||||
|
||||
|
||||
class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.add_interface'
|
||||
cls = Device
|
||||
form = forms.InterfaceBulkCreateForm
|
||||
template_name = 'dcim/interface_add_multi.html'
|
||||
default_redirect_url = 'dcim:device_list'
|
||||
|
||||
def update_objects(self, pk_list, form, fields):
|
||||
|
||||
selected_devices = Device.objects.filter(pk__in=pk_list)
|
||||
interfaces = []
|
||||
|
||||
for device in selected_devices:
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
iface_form = forms.InterfaceForm({
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
'mac_address': form.cleaned_data['mac_address'],
|
||||
'form_factor': form.cleaned_data['form_factor'],
|
||||
'mgmt_only': form.cleaned_data['mgmt_only'],
|
||||
'description': form.cleaned_data['description'],
|
||||
})
|
||||
if iface_form.is_valid():
|
||||
interfaces.append(iface_form.save(commit=False))
|
||||
else:
|
||||
form.add_error(None, "Duplicate interface {} found for device {}".format(name, device))
|
||||
|
||||
if not form.errors:
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
messages.success(self.request, "Added {} interfaces to {} devices".format(len(interfaces),
|
||||
len(selected_devices)))
|
||||
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
cls = Interface
|
||||
parent_cls = Device
|
||||
form = forms.InterfaceBulkEditForm
|
||||
template_name = 'dcim/interface_bulk_edit.html'
|
||||
|
||||
|
||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -1458,7 +1352,7 @@ def devicebay_add(request, pk):
|
||||
|
||||
if not form.errors:
|
||||
DeviceBay.objects.bulk_create(device_bays)
|
||||
messages.success(request, "Added {} device bay(s) to {}".format(len(device_bays), device))
|
||||
messages.success(request, u"Added {} device bay(s) to {}.".format(len(device_bays), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:devicebay_add', pk=device.pk)
|
||||
else:
|
||||
@@ -1467,55 +1361,23 @@ def devicebay_add(request, pk):
|
||||
else:
|
||||
form = forms.DeviceBayCreateForm()
|
||||
|
||||
return render(request, 'dcim/devicebay_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Device Bay',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_devicebay')
|
||||
def devicebay_edit(request, pk):
|
||||
|
||||
devicebay = get_object_or_404(DeviceBay, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.DeviceBayForm(request.POST, instance=devicebay)
|
||||
if form.is_valid():
|
||||
devicebay = form.save()
|
||||
messages.success(request, "Modified {} bay {}".format(devicebay.device.name, devicebay.name))
|
||||
return redirect('dcim:device', pk=devicebay.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.DeviceBayForm(instance=devicebay)
|
||||
|
||||
return render(request, 'dcim/devicebay_edit.html', {
|
||||
'devicebay': devicebay,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': devicebay.device.pk}),
|
||||
})
|
||||
class DeviceBayEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_devicebay'
|
||||
model = DeviceBay
|
||||
form_class = forms.DeviceBayForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_devicebay')
|
||||
def devicebay_delete(request, pk):
|
||||
|
||||
devicebay = get_object_or_404(DeviceBay, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
devicebay.delete()
|
||||
messages.success(request, "Device bay {} has been deleted from {}".format(devicebay, devicebay.device))
|
||||
return redirect('dcim:device', pk=devicebay.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/devicebay_delete.html', {
|
||||
'devicebay': devicebay,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': devicebay.device.pk}),
|
||||
})
|
||||
class DeviceBayDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_devicebay'
|
||||
model = DeviceBay
|
||||
|
||||
|
||||
@permission_required('dcim.change_devicebay')
|
||||
@@ -1531,7 +1393,7 @@ def devicebay_populate(request, pk):
|
||||
device_bay.save()
|
||||
|
||||
if not form.errors:
|
||||
messages.success(request, "Added {} to {}".format(device_bay.installed_device, device_bay))
|
||||
messages.success(request, u"Added {} to {}.".format(device_bay.installed_device, device_bay))
|
||||
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1555,7 +1417,7 @@ def devicebay_depopulate(request, pk):
|
||||
removed_device = device_bay.installed_device
|
||||
device_bay.installed_device = None
|
||||
device_bay.save()
|
||||
messages.success(request, "{} has been removed from {}".format(removed_device, device_bay))
|
||||
messages.success(request, u"{} has been removed from {}.".format(removed_device, device_bay))
|
||||
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1587,7 +1449,7 @@ def interfaceconnection_add(request, pk):
|
||||
form = forms.InterfaceConnectionForm(device, request.POST)
|
||||
if form.is_valid():
|
||||
interfaceconnection = form.save()
|
||||
messages.success(request, "Connected {0} {1} to {2} {3}".format(
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
interfaceconnection.interface_a.device,
|
||||
interfaceconnection.interface_a,
|
||||
interfaceconnection.interface_b.device,
|
||||
@@ -1627,7 +1489,7 @@ def interfaceconnection_delete(request, pk):
|
||||
form = forms.InterfaceConnectionDeletionForm(request.POST)
|
||||
if form.is_valid():
|
||||
interfaceconnection.delete()
|
||||
messages.success(request, "Deleted the connection between {0} {1} and {2} {3}".format(
|
||||
messages.success(request, u"Deleted the connection between {} {} and {} {}.".format(
|
||||
interfaceconnection.interface_a.device,
|
||||
interfaceconnection.interface_a,
|
||||
interfaceconnection.interface_b.device,
|
||||
@@ -1699,7 +1561,7 @@ class InterfaceConnectionsListView(ObjectListView):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
@permission_required('ipam.add_ipaddress')
|
||||
@permission_required(['dcim.change_device', 'ipam.add_ipaddress'])
|
||||
def ipaddress_assign(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
@@ -1711,8 +1573,8 @@ def ipaddress_assign(request, pk):
|
||||
ipaddress = form.save(commit=False)
|
||||
ipaddress.interface = form.cleaned_data['interface']
|
||||
ipaddress.save()
|
||||
messages.success(request, "Added new IP address {0} to interface {1}".format(ipaddress,
|
||||
ipaddress.interface))
|
||||
form.save_custom_fields()
|
||||
messages.success(request, u"Added new IP address {} to interface {}.".format(ipaddress, ipaddress.interface))
|
||||
|
||||
if form.cleaned_data['set_as_primary']:
|
||||
if ipaddress.family == 4:
|
||||
@@ -1751,7 +1613,7 @@ def module_add(request, pk):
|
||||
module = form.save(commit=False)
|
||||
module.device = device
|
||||
module.save()
|
||||
messages.success(request, "Added module {} to {}".format(module.name, module.device.name))
|
||||
messages.success(request, u"Added module {} to {}".format(module.name, module.device.name))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:module_add', pk=module.device.pk)
|
||||
else:
|
||||
@@ -1760,52 +1622,20 @@ def module_add(request, pk):
|
||||
else:
|
||||
form = forms.ModuleForm()
|
||||
|
||||
return render(request, 'dcim/module_edit.html', {
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Module',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device_inventory', kwargs={'pk': device.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('dcim.change_module')
|
||||
def module_edit(request, pk):
|
||||
|
||||
module = get_object_or_404(Module, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.ModuleForm(request.POST, instance=module)
|
||||
if form.is_valid():
|
||||
module = form.save()
|
||||
messages.success(request, "Modified {} module {}".format(module.device.name, module.name))
|
||||
return redirect('dcim:device_inventory', pk=module.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.ModuleForm(instance=module)
|
||||
|
||||
return render(request, 'dcim/module_edit.html', {
|
||||
'module': module,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device_inventory', kwargs={'pk': module.device.pk}),
|
||||
})
|
||||
class ModuleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_module'
|
||||
model = Module
|
||||
form_class = forms.ModuleForm
|
||||
|
||||
|
||||
@permission_required('dcim.delete_module')
|
||||
def module_delete(request, pk):
|
||||
|
||||
module = get_object_or_404(Module, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
module.delete()
|
||||
messages.success(request, "Module {} has been deleted from {}".format(module, module.device))
|
||||
return redirect('dcim:device_inventory', pk=module.device.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'dcim/module_delete.html', {
|
||||
'module': module,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device_inventory', kwargs={'pk': module.device.pk}),
|
||||
})
|
||||
class ModuleDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_module'
|
||||
model = Module
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -80,7 +80,7 @@ class TopologyMapView(APIView):
|
||||
|
||||
# Add each device to the graph
|
||||
devices = []
|
||||
for query in device_set.split(','):
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
devices += Device.objects.filter(name__regex=query)
|
||||
for d in devices:
|
||||
subgraph.node(d.name)
|
||||
@@ -94,7 +94,7 @@ class TopologyMapView(APIView):
|
||||
# Compile list of all devices
|
||||
device_superset = Q()
|
||||
for device_set in tmap.device_sets:
|
||||
for query in device_set.split(','):
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
device_superset = device_superset | Q(name__regex=query)
|
||||
|
||||
# Add all connections to the graph
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections import OrderedDict
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from utilities.forms import LaxURLField
|
||||
from utilities.forms import BulkEditForm, LaxURLField
|
||||
from .models import (
|
||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
|
||||
)
|
||||
@@ -49,9 +49,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
# Select
|
||||
elif cf.type == CF_TYPE_SELECT:
|
||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||
if not cf.required:
|
||||
choices = [(0, 'None')] + choices
|
||||
if bulk_edit or filterable_only:
|
||||
if not cf.required or bulk_edit or filterable_only:
|
||||
choices = [(None, '---------')] + choices
|
||||
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
||||
|
||||
@@ -73,10 +71,10 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
|
||||
|
||||
class CustomFieldForm(forms.ModelForm):
|
||||
custom_fields = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
self.custom_fields = []
|
||||
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
|
||||
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
||||
@@ -126,22 +124,24 @@ class CustomFieldForm(forms.ModelForm):
|
||||
return obj
|
||||
|
||||
|
||||
class CustomFieldBulkEditForm(forms.Form):
|
||||
custom_fields = []
|
||||
|
||||
def __init__(self, model, *args, **kwargs):
|
||||
|
||||
self.obj_type = ContentType.objects.get_for_model(model)
|
||||
class CustomFieldBulkEditForm(BulkEditForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomFieldBulkEditForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.custom_fields = []
|
||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||
|
||||
# Add all applicable CustomFields to the form
|
||||
custom_fields = []
|
||||
for name, field in get_custom_fields_for_model(self.obj_type, bulk_edit=True).items():
|
||||
custom_fields = get_custom_fields_for_model(self.obj_type, bulk_edit=True).items()
|
||||
for name, field in custom_fields:
|
||||
# Annotate non-required custom fields as nullable
|
||||
if not field.required:
|
||||
self.nullable_fields.append(name)
|
||||
field.required = False
|
||||
self.fields[name] = field
|
||||
custom_fields.append(name)
|
||||
self.custom_fields = custom_fields
|
||||
# Annotate this as a custom field
|
||||
self.custom_fields.append(name)
|
||||
|
||||
|
||||
class CustomFieldFilterForm(forms.Form):
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-11-03 18:33
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from extras.models import TopologyMap
|
||||
|
||||
|
||||
def commas_to_semicolons(apps, schema_editor):
|
||||
for tm in TopologyMap.objects.filter(device_patterns__contains=','):
|
||||
tm.device_patterns = tm.device_patterns.replace(',', ';')
|
||||
tm.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0003_exporttemplate_add_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='topologymap',
|
||||
name='device_patterns',
|
||||
field=models.TextField(help_text=b'Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'),
|
||||
),
|
||||
migrations.RunPython(commas_to_semicolons),
|
||||
]
|
||||
@@ -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):
|
||||
@@ -268,10 +268,11 @@ class TopologyMap(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True)
|
||||
device_patterns = models.TextField(help_text="Identify devices to include in the diagram using regular expressions,"
|
||||
"one per line. Each line will result in a new tier of the drawing. "
|
||||
"Separate multiple regexes on a line using commas. Devices will be "
|
||||
"rendered in the order they are defined.")
|
||||
device_patterns = models.TextField(
|
||||
help_text="Identify devices to include in the diagram using regular expressions, one per line. Each line will "
|
||||
"result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. "
|
||||
"Devices will be rendered in the order they are defined."
|
||||
)
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
#
|
||||
@@ -159,8 +159,8 @@ class IPAddressSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside',
|
||||
'custom_fields']
|
||||
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside',
|
||||
'nat_outside', 'custom_fields']
|
||||
|
||||
|
||||
class IPAddressNestedSerializer(IPAddressSerializer):
|
||||
|
||||
@@ -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',
|
||||
@@ -232,7 +239,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['q', 'family', 'device_id', 'device', 'interface_id']
|
||||
fields = ['q', 'family', 'status', 'device_id', 'device', 'interface_id']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from dcim.models import Site, Rack, Device, Interface
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField,
|
||||
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField, add_blank_choice,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup,
|
||||
VLAN_STATUS_CHOICES, VRF,
|
||||
)
|
||||
|
||||
|
||||
FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
|
||||
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
|
||||
IP_FAMILY_CHOICES = [
|
||||
('', 'All'),
|
||||
(4, 'IPv4'),
|
||||
@@ -23,18 +21,6 @@ IP_FAMILY_CHOICES = [
|
||||
]
|
||||
|
||||
|
||||
def bulkedit_vrf_choices():
|
||||
"""
|
||||
Include an option to assign the object to the global table.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'Global'),
|
||||
]
|
||||
choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
@@ -67,9 +53,12 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'description']
|
||||
|
||||
|
||||
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = VRF
|
||||
@@ -86,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'),
|
||||
]))
|
||||
|
||||
|
||||
#
|
||||
@@ -124,6 +121,9 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
date_added = forms.DateField(required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['date_added', 'description']
|
||||
|
||||
|
||||
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Aggregate
|
||||
@@ -180,16 +180,6 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||
else:
|
||||
self.fields['vlan'].choices = []
|
||||
|
||||
def clean_prefix(self):
|
||||
prefix = self.cleaned_data['prefix']
|
||||
if prefix.version == 4 and prefix.prefixlen == 32:
|
||||
raise forms.ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 "
|
||||
"addresses instead.")
|
||||
elif prefix.version == 6 and prefix.prefixlen == 128:
|
||||
raise forms.ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 "
|
||||
"addresses instead.")
|
||||
return prefix
|
||||
|
||||
|
||||
class PrefixFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
@@ -238,12 +228,11 @@ class PrefixFromCSVForm(forms.ModelForm):
|
||||
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
m = super(PrefixFromCSVForm, self).save(commit=False)
|
||||
|
||||
# Assign Prefix status by name
|
||||
m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
if kwargs.get('commit'):
|
||||
m.save()
|
||||
return m
|
||||
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
|
||||
return super(PrefixFromCSVForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class PrefixImportForm(BulkImportForm, BootstrapMixin):
|
||||
@@ -253,12 +242,15 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin):
|
||||
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['site', 'vrf', 'tenant', 'role', 'description']
|
||||
|
||||
|
||||
def prefix_status_choices():
|
||||
status_counts = {}
|
||||
@@ -294,20 +286,17 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
widget=forms.Select(attrs={'filter-for': 'nat_device'}))
|
||||
nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||
widget=APISelect(api_url='/api/dcim/devices/?site_id={{nat_site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'nat_inside'}))
|
||||
livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch(
|
||||
query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address')
|
||||
)
|
||||
nat_inside = forms.ModelChoiceField(queryset=IPAddress.objects.all(), required=False, label='NAT (Inside)',
|
||||
widget=APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
|
||||
display_field='address'))
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description']
|
||||
help_texts = {
|
||||
'address': "IPv4 or IPv6 address and mask",
|
||||
'vrf': "VRF (if applicable)",
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'nat_inside', 'description']
|
||||
widgets = {
|
||||
'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -350,11 +339,35 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
self.fields['nat_inside'].choices = []
|
||||
|
||||
|
||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', display_field='display_name', attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', display_field='display_name', attrs={'filter-for': 'interface'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/'))
|
||||
set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(IPAddressAssignForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack'].choices = []
|
||||
self.fields['device'].choices = []
|
||||
self.fields['interface'].choices = []
|
||||
|
||||
|
||||
class IPAddressFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
error_messages={'invalid_choice': 'VRF not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in IPADDRESS_STATUS_CHOICES])
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
interface_name = forms.CharField(required=False)
|
||||
@@ -362,7 +375,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description']
|
||||
fields = ['address', 'vrf', 'tenant', 'status_name', 'device', 'interface_name', 'is_primary', 'description']
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -385,7 +398,10 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
||||
if is_primary and not device:
|
||||
self.add_error('is_primary', "No device specified; cannot set as primary IP")
|
||||
|
||||
def save(self, commit=True):
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Assign status by name
|
||||
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
|
||||
# Set interface
|
||||
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
|
||||
@@ -398,7 +414,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
||||
elif self.instance.address.version == 6:
|
||||
self.instance.primary_ip6_for = self.cleaned_data['device']
|
||||
|
||||
return super(IPAddressFromCSVForm, self).save(commit=commit)
|
||||
return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
||||
@@ -407,10 +423,21 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['vrf', 'tenant', 'description']
|
||||
|
||||
|
||||
def ipaddress_status_choices():
|
||||
status_counts = {}
|
||||
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
status_counts[status['status']] = status['count']
|
||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
|
||||
|
||||
|
||||
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = IPAddress
|
||||
@@ -422,6 +449,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
label='VRF', null_option=(0, 'Global'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
||||
to_field_name='slug', null_option=(0, 'None'))
|
||||
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
|
||||
|
||||
|
||||
#
|
||||
@@ -479,7 +507,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
class VLANFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'VLAN group not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
@@ -509,11 +537,14 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(VLAN_STATUS_CHOICES), required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['group', 'tenant', 'role', 'description']
|
||||
|
||||
|
||||
def vlan_status_choices():
|
||||
status_counts = {}
|
||||
|
||||
20
netbox/ipam/migrations/0009_ipaddress_add_status.py
Normal file
20
netbox/ipam/migrations/0009_ipaddress_add_status.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-10-21 15:44
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0008_prefix_change_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, b'Active'), (2, b'Reserved'), (5, b'DHCP')], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
]
|
||||
27
netbox/ipam/migrations/0010_ipaddress_help_texts.py
Normal file
27
netbox/ipam/migrations/0010_ipaddress_help_texts.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-11-01 17:46
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import ipam.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0009_ipaddress_add_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='address',
|
||||
field=ipam.fields.IPAddressField(help_text=b'IPv4 or IPv6 address (with mask)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='nat_inside',
|
||||
field=models.OneToOneField(blank=True, help_text=b'The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name=b'NAT (Inside)'),
|
||||
),
|
||||
]
|
||||
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'),
|
||||
),
|
||||
]
|
||||
@@ -22,17 +22,33 @@ AF_CHOICES = (
|
||||
(6, 'IPv6'),
|
||||
)
|
||||
|
||||
PREFIX_STATUS_CONTAINER = 0
|
||||
PREFIX_STATUS_ACTIVE = 1
|
||||
PREFIX_STATUS_RESERVED = 2
|
||||
PREFIX_STATUS_DEPRECATED = 3
|
||||
PREFIX_STATUS_CHOICES = (
|
||||
(0, 'Container'),
|
||||
(1, 'Active'),
|
||||
(2, 'Reserved'),
|
||||
(3, 'Deprecated')
|
||||
(PREFIX_STATUS_CONTAINER, 'Container'),
|
||||
(PREFIX_STATUS_ACTIVE, 'Active'),
|
||||
(PREFIX_STATUS_RESERVED, 'Reserved'),
|
||||
(PREFIX_STATUS_DEPRECATED, 'Deprecated')
|
||||
)
|
||||
|
||||
IPADDRESS_STATUS_ACTIVE = 1
|
||||
IPADDRESS_STATUS_RESERVED = 2
|
||||
IPADDRESS_STATUS_DHCP = 5
|
||||
IPADDRESS_STATUS_CHOICES = (
|
||||
(IPADDRESS_STATUS_ACTIVE, 'Active'),
|
||||
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
|
||||
(IPADDRESS_STATUS_DHCP, 'DHCP')
|
||||
)
|
||||
|
||||
VLAN_STATUS_ACTIVE = 1
|
||||
VLAN_STATUS_RESERVED = 2
|
||||
VLAN_STATUS_DEPRECATED = 3
|
||||
VLAN_STATUS_CHOICES = (
|
||||
(1, 'Active'),
|
||||
(2, 'Reserved'),
|
||||
(3, 'Deprecated')
|
||||
(VLAN_STATUS_ACTIVE, 'Active'),
|
||||
(VLAN_STATUS_RESERVED, 'Reserved'),
|
||||
(VLAN_STATUS_DEPRECATED, 'Deprecated')
|
||||
)
|
||||
|
||||
STATUS_CHOICE_CLASSES = {
|
||||
@@ -40,6 +56,8 @@ STATUS_CHOICE_CLASSES = {
|
||||
1: 'primary',
|
||||
2: 'info',
|
||||
3: 'danger',
|
||||
4: 'warning',
|
||||
5: 'success',
|
||||
}
|
||||
|
||||
|
||||
@@ -85,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']
|
||||
@@ -131,16 +151,22 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||
if self.pk:
|
||||
covering_aggregates = covering_aggregates.exclude(pk=self.pk)
|
||||
if covering_aggregates:
|
||||
raise ValidationError("{} is already covered by an existing aggregate ({})"
|
||||
.format(self.prefix, covering_aggregates[0]))
|
||||
raise ValidationError({
|
||||
'prefix': "Aggregates cannot overlap. {} is already covered by an existing aggregate ({}).".format(
|
||||
self.prefix, covering_aggregates[0]
|
||||
)
|
||||
})
|
||||
|
||||
# Ensure that the aggregate being added does not cover an existing aggregate
|
||||
covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix))
|
||||
if self.pk:
|
||||
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
|
||||
if covered_aggregates:
|
||||
raise ValidationError("{} is overlaps with an existing aggregate ({})"
|
||||
.format(self.prefix, covered_aggregates[0]))
|
||||
raise ValidationError({
|
||||
'prefix': "Aggregates cannot overlap. {} covers an existing aggregate ({}).".format(
|
||||
self.prefix, covered_aggregates[0]
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.prefix:
|
||||
@@ -260,14 +286,17 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('ipam:prefix', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Disallow host masks
|
||||
if self.prefix:
|
||||
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
|
||||
raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses "
|
||||
"instead.")
|
||||
raise ValidationError({
|
||||
'prefix': "Cannot create host addresses (/32) as prefixes. Create an IPv4 address instead."
|
||||
})
|
||||
elif self.prefix.version == 6 and self.prefix.prefixlen == 128:
|
||||
raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses "
|
||||
"instead.")
|
||||
raise ValidationError({
|
||||
'prefix': "Cannot create host addresses (/128) as prefixes. Create an IPv6 address instead."
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.prefix:
|
||||
@@ -329,14 +358,16 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
|
||||
"""
|
||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
|
||||
address = IPAddressField()
|
||||
address = IPAddressField(help_text="IPv4 or IPv6 address (with mask)")
|
||||
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
|
||||
verbose_name='VRF')
|
||||
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
|
||||
status = models.PositiveSmallIntegerField('Status', choices=IPADDRESS_STATUS_CHOICES, default=1)
|
||||
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
|
||||
null=True)
|
||||
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
|
||||
null=True, verbose_name='NAT IP (inside)')
|
||||
null=True, verbose_name='NAT (Inside)',
|
||||
help_text="The IP for which this address is the \"outside\" IP")
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
@@ -360,13 +391,16 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
duplicate_ips = IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip))\
|
||||
.exclude(pk=self.pk)
|
||||
if duplicate_ips:
|
||||
raise ValidationError("Duplicate IP address found in VRF {}: {}".format(self.vrf,
|
||||
duplicate_ips.first()))
|
||||
raise ValidationError({
|
||||
'address': "Duplicate IP address found in VRF {}: {}".format(self.vrf, duplicate_ips.first())
|
||||
})
|
||||
elif not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE:
|
||||
duplicate_ips = IPAddress.objects.filter(vrf=None, address__net_host=str(self.address.ip))\
|
||||
.exclude(pk=self.pk)
|
||||
if duplicate_ips:
|
||||
raise ValidationError("Duplicate IP address found in global table: {}".format(duplicate_ips.first()))
|
||||
raise ValidationError({
|
||||
'address': "Duplicate IP address found in global table: {}".format(duplicate_ips.first())
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.address:
|
||||
@@ -387,6 +421,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
str(self.address),
|
||||
self.vrf.rd if self.vrf else '',
|
||||
self.tenant.name if self.tenant else '',
|
||||
self.get_status_display(),
|
||||
self.device.identifier if self.device else '',
|
||||
self.interface.name if self.interface else '',
|
||||
'True' if is_primary else '',
|
||||
@@ -399,6 +434,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
return self.interface.device
|
||||
return None
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
|
||||
class VLANGroup(models.Model):
|
||||
"""
|
||||
@@ -465,7 +503,9 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
# Validate VLAN group
|
||||
if self.group and self.group.site != self.site:
|
||||
raise ValidationError("VLAN group must belong to the assigned site ({}).".format(self.site))
|
||||
raise ValidationError({
|
||||
'group': "VLAN group must belong to the assigned site ({}).".format(self.site)
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
|
||||
@@ -6,6 +6,25 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
RIR_UTILIZATION = """
|
||||
<div class="progress">
|
||||
{% if record.stats.total %}
|
||||
<div class="progress-bar" role="progressbar" style="width: {{ record.stats.percentages.active }}%;">
|
||||
<span class="sr-only">{{ record.stats.percentages.active }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar progress-bar-info" role="progressbar" style="width: {{ record.stats.percentages.reserved }}%;">
|
||||
<span class="sr-only">{{ record.stats.percentages.reserved }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar progress-bar-danger" role="progressbar" style="width: {{ record.stats.percentages.deprecated }}%;">
|
||||
<span class="sr-only">{{ record.stats.percentages.deprecated }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ record.stats.percentages.available }}%;">
|
||||
<span class="sr-only">{{ record.stats.percentages.available }}%</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
RIR_ACTIONS = """
|
||||
{% if perms.ipam.change_rir %}
|
||||
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@@ -14,7 +33,7 @@ RIR_ACTIONS = """
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph record.get_utilization %}
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
@@ -107,13 +126,25 @@ 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')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
|
||||
footer=lambda table: sum(r.stats['total'] for r in table.data))
|
||||
stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
|
||||
footer=lambda table: sum(r.stats['active'] for r in table.data))
|
||||
stats_reserved = tables.Column(accessor='stats.reserved', verbose_name='Reserved',
|
||||
footer=lambda table: sum(r.stats['reserved'] for r in table.data))
|
||||
stats_deprecated = tables.Column(accessor='stats.deprecated', verbose_name='Deprecated',
|
||||
footer=lambda table: sum(r.stats['deprecated'] for r in table.data))
|
||||
stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
|
||||
footer=lambda table: sum(r.stats['available'] for r in table.data))
|
||||
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
|
||||
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
fields = ('pk', 'name', 'aggregate_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
|
||||
'stats_deprecated', 'stats_available', 'utilization', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -125,13 +156,13 @@ class AggregateTable(BaseTable):
|
||||
prefix = tables.LinkColumn('ipam:aggregate', args=[Accessor('pk')], verbose_name='Aggregate')
|
||||
rir = tables.Column(verbose_name='RIR')
|
||||
child_count = tables.Column(verbose_name='Prefixes')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
||||
description = tables.Column(orderable=False, verbose_name='Description')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Aggregate
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -193,6 +224,7 @@ class PrefixBriefTable(BaseTable):
|
||||
class IPAddressTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
|
||||
@@ -202,7 +234,7 @@ class IPAddressTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ urlpatterns = [
|
||||
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/$', views.ipaddress, name='ipaddress'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/remove/$', views.ipaddress_remove, name='ipaddress_remove'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||
|
||||
# VLAN groups
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import netaddr
|
||||
from django_tables2 import RequestConfig
|
||||
import netaddr
|
||||
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count, Q
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
|
||||
from . import filters, forms, tables
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||
from .models import Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
def add_available_prefixes(parent, prefix_list):
|
||||
@@ -112,7 +116,7 @@ class VRFEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = VRF
|
||||
form_class = forms.VRFForm
|
||||
template_name = 'ipam/vrf_edit.html'
|
||||
cancel_url = 'ipam:vrf_list'
|
||||
obj_list_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
@@ -149,17 +153,95 @@ 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'
|
||||
|
||||
def alter_queryset(self, request):
|
||||
|
||||
if request.GET.get('family') == '6':
|
||||
family = 6
|
||||
denominator = 2 ** 64 # Count /64s for IPv6 rather than individual IPs
|
||||
else:
|
||||
family = 4
|
||||
denominator = 1
|
||||
|
||||
rirs = []
|
||||
for rir in self.queryset:
|
||||
|
||||
stats = {
|
||||
'total': 0,
|
||||
'active': 0,
|
||||
'reserved': 0,
|
||||
'deprecated': 0,
|
||||
'available': 0,
|
||||
}
|
||||
aggregate_list = Aggregate.objects.filter(family=family, rir=rir)
|
||||
for aggregate in aggregate_list:
|
||||
|
||||
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
|
||||
|
||||
# Find all consumed space for each prefix status (we ignore containers for this purpose).
|
||||
active_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)])
|
||||
reserved_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)])
|
||||
deprecated_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)])
|
||||
|
||||
# Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
|
||||
available_prefixes = (
|
||||
netaddr.IPSet([aggregate.prefix]) -
|
||||
netaddr.IPSet(active_prefixes) -
|
||||
netaddr.IPSet(reserved_prefixes) -
|
||||
netaddr.IPSet(deprecated_prefixes)
|
||||
)
|
||||
|
||||
# Add the size of each metric to the RIR total.
|
||||
stats['total'] += aggregate.prefix.size / denominator
|
||||
stats['active'] += netaddr.IPSet(active_prefixes).size / denominator
|
||||
stats['reserved'] += netaddr.IPSet(reserved_prefixes).size / denominator
|
||||
stats['deprecated'] += netaddr.IPSet(deprecated_prefixes).size / denominator
|
||||
stats['available'] += available_prefixes.size / denominator
|
||||
|
||||
# Calculate the percentage of total space for each prefix status.
|
||||
total = float(stats['total'])
|
||||
stats['percentages'] = {
|
||||
'active': float('{:.2f}'.format(stats['active'] / total * 100)) if total else 0,
|
||||
'reserved': float('{:.2f}'.format(stats['reserved'] / total * 100)) if total else 0,
|
||||
'deprecated': float('{:.2f}'.format(stats['deprecated'] / total * 100)) if total else 0,
|
||||
}
|
||||
stats['percentages']['available'] = (
|
||||
100 -
|
||||
stats['percentages']['active'] -
|
||||
stats['percentages']['reserved'] -
|
||||
stats['percentages']['deprecated']
|
||||
)
|
||||
rir.stats = stats
|
||||
rirs.append(rir)
|
||||
|
||||
return rirs
|
||||
|
||||
def extra_context(self):
|
||||
|
||||
totals = {
|
||||
'total': sum([rir.stats['total'] for rir in self.queryset]),
|
||||
'active': sum([rir.stats['active'] for rir in self.queryset]),
|
||||
'reserved': sum([rir.stats['reserved'] for rir in self.queryset]),
|
||||
'deprecated': sum([rir.stats['deprecated'] for rir in self.queryset]),
|
||||
'available': sum([rir.stats['available'] for rir in self.queryset]),
|
||||
}
|
||||
|
||||
return {
|
||||
'totals': totals,
|
||||
}
|
||||
|
||||
|
||||
class RIREditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_rir'
|
||||
model = RIR
|
||||
form_class = forms.RIRForm
|
||||
success_url = 'ipam:rir_list'
|
||||
cancel_url = 'ipam:rir_list'
|
||||
obj_list_url = 'ipam:rir_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -224,7 +306,7 @@ class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Aggregate
|
||||
form_class = forms.AggregateForm
|
||||
template_name = 'ipam/aggregate_edit.html'
|
||||
cancel_url = 'ipam:aggregate_list'
|
||||
obj_list_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
@@ -270,8 +352,8 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_role'
|
||||
model = Role
|
||||
form_class = forms.RoleForm
|
||||
success_url = 'ipam:role_list'
|
||||
cancel_url = 'ipam:role_list'
|
||||
obj_list_url = 'ipam:role_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -355,7 +437,7 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
form_class = forms.PrefixForm
|
||||
template_name = 'ipam/prefix_edit.html'
|
||||
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
|
||||
cancel_url = 'ipam:prefix_list'
|
||||
obj_list_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
@@ -446,13 +528,80 @@ def ipaddress(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@permission_required(['dcim.change_device', 'ipam.change_ipaddress'])
|
||||
def ipaddress_assign(request, pk):
|
||||
|
||||
ipaddress = get_object_or_404(IPAddress, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.IPAddressAssignForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
interface = form.cleaned_data['interface']
|
||||
ipaddress.interface = interface
|
||||
ipaddress.save()
|
||||
messages.success(request, u"Assigned IP address {} to interface {}.".format(ipaddress, ipaddress.interface))
|
||||
|
||||
if form.cleaned_data['set_as_primary']:
|
||||
device = interface.device
|
||||
if ipaddress.family == 4:
|
||||
device.primary_ip4 = ipaddress
|
||||
elif ipaddress.family == 6:
|
||||
device.primary_ip6 = ipaddress
|
||||
device.save()
|
||||
|
||||
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
||||
|
||||
else:
|
||||
form = forms.IPAddressAssignForm()
|
||||
|
||||
return render(request, 'ipam/ipaddress_assign.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required(['dcim.change_device', 'ipam.change_ipaddress'])
|
||||
def ipaddress_remove(request, pk):
|
||||
|
||||
ipaddress = get_object_or_404(IPAddress, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
device = ipaddress.interface.device
|
||||
ipaddress.interface = None
|
||||
ipaddress.save()
|
||||
messages.success(request, u"Removed IP address {} from {}.".format(ipaddress, device))
|
||||
|
||||
if device.primary_ip4 == ipaddress.pk:
|
||||
device.primary_ip4 = None
|
||||
device.save()
|
||||
elif device.primary_ip6 == ipaddress.pk:
|
||||
device.primary_ip6 = None
|
||||
device.save()
|
||||
|
||||
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'ipam/ipaddress_unassign.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
|
||||
})
|
||||
|
||||
|
||||
class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_ipaddress'
|
||||
model = IPAddress
|
||||
form_class = forms.IPAddressForm
|
||||
fields_initial = ['address', 'vrf']
|
||||
template_name = 'ipam/ipaddress_edit.html'
|
||||
cancel_url = 'ipam:ipaddress_list'
|
||||
obj_list_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
@@ -515,8 +664,8 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_vlangroup'
|
||||
model = VLANGroup
|
||||
form_class = forms.VLANGroupForm
|
||||
success_url = 'ipam:vlangroup_list'
|
||||
cancel_url = 'ipam:vlangroup_list'
|
||||
obj_list_url = 'ipam:vlangroup_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -555,7 +704,7 @@ class VLANEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = VLAN
|
||||
form_class = forms.VLANForm
|
||||
template_name = 'ipam/vlan_edit.html'
|
||||
cancel_url = 'ipam:vlan_list'
|
||||
obj_list_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
||||
#
|
||||
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
||||
ALLOWED_HOSTS = [os.environ.get('ALLOWED_HOSTS', '')]
|
||||
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(' ')
|
||||
|
||||
# PostgreSQL database configuration.
|
||||
DATABASE = {
|
||||
|
||||
@@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.6.2-r1'
|
||||
VERSION = '1.7.3'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
@@ -162,7 +162,7 @@ USE_TZ = True
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
||||
STATIC_ROOT = BASE_DIR + '/static/'
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = '/{}static/'.format(BASE_PATH)
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, "project-static"),
|
||||
)
|
||||
@@ -176,8 +176,7 @@ MESSAGE_TAGS = {
|
||||
}
|
||||
|
||||
# Authentication URLs
|
||||
LOGIN_URL = '/login/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGIN_URL = '/{}login/'.format(BASE_PATH)
|
||||
|
||||
# Secrets
|
||||
SECRETS_MIN_PUBKEY_SIZE = 2048
|
||||
@@ -186,10 +185,12 @@ SECRETS_MIN_PUBKEY_SIZE = 2048
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',)
|
||||
}
|
||||
if LOGIN_REQUIRED:
|
||||
REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ('rest_framework.permissions.IsAuthenticated',)
|
||||
|
||||
# Swagger settings (API docs)
|
||||
SWAGGER_SETTINGS = {
|
||||
'base_path': '{}/api/docs'.format(ALLOWED_HOSTS[0]),
|
||||
'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.views.defaults import page_not_found
|
||||
|
||||
from views import home, trigger_500, handle_500
|
||||
from views import home, handle_500, trigger_500
|
||||
from users.views import login, logout
|
||||
|
||||
|
||||
@@ -36,7 +35,6 @@ _patterns = [
|
||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
|
||||
# Error testing
|
||||
url(r'^404/$', page_not_found),
|
||||
url(r'^500/$', trigger_500),
|
||||
|
||||
# Admin
|
||||
|
||||
@@ -47,16 +47,20 @@ def home(request):
|
||||
})
|
||||
|
||||
|
||||
def trigger_500(request):
|
||||
"""Hot-wired method of triggering a server error to test reporting."""
|
||||
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
|
||||
"person you are.")
|
||||
|
||||
|
||||
def handle_500(request):
|
||||
"""Custom server error handler"""
|
||||
"""
|
||||
Custom server error handler
|
||||
"""
|
||||
type_, error, traceback = sys.exc_info()
|
||||
return render(request, '500.html', {
|
||||
'exception': str(type_),
|
||||
'error': error,
|
||||
}, status=500)
|
||||
|
||||
|
||||
def trigger_500(request):
|
||||
"""
|
||||
Hot-wired method of triggering a server error to test reporting
|
||||
"""
|
||||
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
|
||||
"person you are.")
|
||||
|
||||
@@ -85,6 +85,9 @@ label.required {
|
||||
th.pk, td.pk {
|
||||
width: 30px;
|
||||
}
|
||||
tfoot td {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Paginator */
|
||||
nav ul.pagination {
|
||||
@@ -95,7 +98,7 @@ nav ul.pagination {
|
||||
div.rack_header {
|
||||
margin-left: 36px;
|
||||
text-align: center;
|
||||
width: 200px;
|
||||
width: 230px;
|
||||
}
|
||||
ul.rack_legend {
|
||||
float: left;
|
||||
@@ -123,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; }
|
||||
@@ -244,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(
|
||||
@@ -269,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(
|
||||
@@ -279,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 |
@@ -1,6 +1,6 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
// "Toggle all" checkbox in a table header
|
||||
// "Toggle all" checkbox (table header)
|
||||
$('#toggle_all').click(function (event) {
|
||||
$('td input:checkbox[name=pk]').prop('checked', $(this).prop('checked'));
|
||||
if ($(this).is(':checked')) {
|
||||
@@ -16,6 +16,15 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
|
||||
// Simple "Toggle all" button (panel)
|
||||
$('button.toggle').click(function (event) {
|
||||
var selected = $(this).attr('selected');
|
||||
$(this).closest('form').find('input:checkbox[name=pk]').prop('checked', !selected);
|
||||
$(this).attr('selected', !selected);
|
||||
$(this).children('span').toggleClass('glyphicon-unchecked glyphicon-check');
|
||||
return false;
|
||||
});
|
||||
|
||||
// Slugify
|
||||
function slugify(s, num_chars) {
|
||||
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
||||
@@ -37,6 +46,11 @@ $(document).ready(function() {
|
||||
})
|
||||
}
|
||||
|
||||
// Bulk edit nullification
|
||||
$('input:checkbox[name=_nullify]').click(function (event) {
|
||||
$('#id_' + this.value).toggle('disabled');
|
||||
});
|
||||
|
||||
// API select widget
|
||||
$('select[filter-for]').change(function () {
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class UserKeyAdmin(admin.ModelAdmin):
|
||||
try:
|
||||
my_userkey = UserKey.objects.get(user=request.user)
|
||||
except UserKey.DoesNotExist:
|
||||
messages.error(request, "You do not have an active User Key.")
|
||||
messages.error(request, u"You do not have an active User Key.")
|
||||
return redirect('/admin/secrets/userkey/')
|
||||
|
||||
if 'activate' in request.POST:
|
||||
@@ -46,7 +46,7 @@ class UserKeyAdmin(admin.ModelAdmin):
|
||||
uk.activate(master_key)
|
||||
return redirect('/admin/secrets/userkey/')
|
||||
except ValueError:
|
||||
messages.error(request, "Invalid private key provided. Unable to retrieve master key.")
|
||||
messages.error(request, u"Invalid private key provided. Unable to retrieve master key.")
|
||||
else:
|
||||
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ def userkey_required():
|
||||
try:
|
||||
uk = UserKey.objects.get(user=request.user)
|
||||
except UserKey.DoesNotExist:
|
||||
messages.warning(request, "This operation requires an active user key, but you don't have one.")
|
||||
messages.warning(request, u"This operation requires an active user key, but you don't have one.")
|
||||
return redirect('users:userkey')
|
||||
if not uk.is_active():
|
||||
messages.warning(request, "This operation is not available. Your user key has not been activated.")
|
||||
messages.warning(request, u"This operation is not available. Your user key has not been activated.")
|
||||
return redirect('users:userkey')
|
||||
return view(request, *args, **kwargs)
|
||||
return wrapped_view
|
||||
|
||||
@@ -5,7 +5,7 @@ from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
||||
from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
||||
|
||||
from .models import Secret, SecretRole, UserKey
|
||||
|
||||
@@ -49,22 +49,23 @@ class SecretRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
class SecretForm(forms.ModelForm, BootstrapMixin):
|
||||
private_key = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
plaintext = forms.CharField(max_length=65535, required=False, label='Plaintext',
|
||||
widget=forms.TextInput(attrs={'class': 'requires-private-key'}))
|
||||
plaintext2 = forms.CharField(max_length=65535, required=False, label='Plaintext (verify)')
|
||||
widget=forms.PasswordInput(attrs={'class': 'requires-private-key'}))
|
||||
plaintext2 = forms.CharField(max_length=65535, required=False, label='Plaintext (verify)',
|
||||
widget=forms.PasswordInput())
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['role', 'name', 'plaintext', 'plaintext2']
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.cleaned_data['plaintext']:
|
||||
validate_rsa_key(self.cleaned_data['private_key'])
|
||||
|
||||
def clean_plaintext2(self):
|
||||
plaintext = self.cleaned_data['plaintext']
|
||||
plaintext2 = self.cleaned_data['plaintext2']
|
||||
if plaintext != plaintext2:
|
||||
raise forms.ValidationError("The two given plaintext values do not match. Please check your input.")
|
||||
if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']:
|
||||
raise forms.ValidationError({
|
||||
'plaintext2': "The two given plaintext values do not match. Please check your input."
|
||||
})
|
||||
|
||||
|
||||
class SecretFromCSVForm(forms.ModelForm):
|
||||
@@ -89,11 +90,14 @@ class SecretImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'}))
|
||||
|
||||
|
||||
class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
||||
class SecretBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
||||
name = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['name']
|
||||
|
||||
|
||||
class SecretFilterForm(forms.Form, BootstrapMixin):
|
||||
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
|
||||
|
||||
@@ -81,24 +81,34 @@ class UserKey(CreatedUpdatedModel):
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
|
||||
# Validate the public key format and length.
|
||||
if self.public_key:
|
||||
|
||||
# Validate the public key format
|
||||
try:
|
||||
pubkey = RSA.importKey(self.public_key)
|
||||
except ValueError:
|
||||
raise ValidationError("Invalid RSA key format.")
|
||||
raise ValidationError({
|
||||
'public_key': "Invalid RSA key format."
|
||||
})
|
||||
except:
|
||||
raise ValidationError("Something went wrong while trying to save your key. Please ensure that you're "
|
||||
"uploading a valid RSA public key in PEM format (no SSH/PGP).")
|
||||
# key.size() returns 1 less than the key modulus
|
||||
pubkey_length = pubkey.size() + 1
|
||||
|
||||
# Validate the public key length
|
||||
pubkey_length = pubkey.size() + 1 # key.size() returns 1 less than the key modulus
|
||||
if pubkey_length < settings.SECRETS_MIN_PUBKEY_SIZE:
|
||||
raise ValidationError("Insufficient key length. Keys must be at least {} bits long."
|
||||
.format(settings.SECRETS_MIN_PUBKEY_SIZE))
|
||||
raise ValidationError({
|
||||
'public_key': "Insufficient key length. Keys must be at least {} bits long.".format(
|
||||
settings.SECRETS_MIN_PUBKEY_SIZE
|
||||
)
|
||||
})
|
||||
# We can't use keys bigger than our master_key_cipher field can hold
|
||||
if pubkey_length > 4096:
|
||||
raise ValidationError("Public key size ({}) is too large. Maximum key size is 4096 bits."
|
||||
.format(pubkey_length))
|
||||
raise ValidationError({
|
||||
'public_key': "Public key size ({}) is too large. Maximum key size is 4096 bits.".format(
|
||||
pubkey_length
|
||||
)
|
||||
})
|
||||
|
||||
super(UserKey, self).clean()
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'secrets.change_secretrole'
|
||||
model = SecretRole
|
||||
form_class = forms.SecretRoleForm
|
||||
success_url = 'secrets:secretrole_list'
|
||||
cancel_url = 'secrets:secretrole_list'
|
||||
obj_list_url = 'secrets:secretrole_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -90,7 +90,7 @@ def secret_add(request, pk):
|
||||
secret.encrypt(master_key)
|
||||
secret.save()
|
||||
|
||||
messages.success(request, "Added new secret: {0}".format(secret))
|
||||
messages.success(request, u"Added new secret: {}.".format(secret))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:device_addsecret', pk=device.pk)
|
||||
else:
|
||||
@@ -135,7 +135,7 @@ def secret_edit(request, pk):
|
||||
else:
|
||||
secret = form.save()
|
||||
|
||||
messages.success(request, "Modified secret {0}".format(secret))
|
||||
messages.success(request, u"Modified secret {}.".format(secret))
|
||||
return redirect('secrets:secret', pk=secret.pk)
|
||||
|
||||
else:
|
||||
@@ -180,7 +180,7 @@ def secret_import(request):
|
||||
new_secrets.append(secret)
|
||||
|
||||
table = tables.SecretTable(new_secrets)
|
||||
messages.success(request, "Imported {} new secrets".format(len(new_secrets)))
|
||||
messages.success(request, u"Imported {} new secrets.".format(len(new_secrets)))
|
||||
|
||||
return render(request, 'import_success.html', {
|
||||
'table': table,
|
||||
|
||||
19
netbox/templates/404.html
Normal file
19
netbox/templates/404.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row" style="margin-top: 150px;">
|
||||
<div class="col-sm-4 col-sm-offset-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong><i class="glyphicon glyphicon-warning-sign"></i> Page Not Found</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
The requested page does not exist.
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'home' %}" class="btn btn-xs btn-primary">Home Page</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -131,6 +131,21 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP Addressing</td>
|
||||
<td>
|
||||
{% if circuit.interface %}
|
||||
{% for ip in circuit.interface.ip_addresses.all %}
|
||||
{% if not forloop.first %}<br />{% endif %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }})
|
||||
{% empty %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cross-Connect</td>
|
||||
<td>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if consoleport.pk %}Editing {{ consoleport.device }} {{ consoleport }}{% else %}Add a Console Port ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if consoleport.pk %}
|
||||
<strong>Editing {{ consoleport }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Console Port</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if consoleport %}{{ consoleport.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if consoleport.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if consoleserverport.pk %}Editing {{ consoleserverport.device }} {{ consoleserverport }}{% else %}Add a Console Server Port ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if consoleserverport.pk %}
|
||||
<strong>Editing {{ consoleserverport }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Console Server Port</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if consoleserverport %}{{ consoleserverport.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if consoleserverport.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -56,7 +56,7 @@
|
||||
<tr>
|
||||
<td>Device Type</td>
|
||||
<td>
|
||||
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type }}</a> ({{ device.device_type.u_height }}U)</span>
|
||||
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type.full_name }}</a> ({{ device.device_type.u_height }}U)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -237,16 +237,14 @@
|
||||
{% for pp in power_ports %}
|
||||
{% include 'dcim/inc/_powerport.html' %}
|
||||
{% empty %}
|
||||
{% if not device.device_type.is_pdu %}
|
||||
<tr>
|
||||
<td colspan="5" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td colspan="5" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||
@@ -261,7 +259,7 @@
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_powerport and not device.device_type.is_pdu %}
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
||||
</a>
|
||||
@@ -295,7 +293,7 @@
|
||||
<td>
|
||||
<a href="{% url 'dcim:rack' pk=rd.rack.pk %}">Rack {{ rd.rack }}</a>
|
||||
</td>
|
||||
<td>{{ rd.device_type }}</td>
|
||||
<td>{{ rd.device_type.full_name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -314,13 +312,16 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bays</strong>
|
||||
{% if perms.dcim.add_devicebay and device_bays|length > 10 %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.add_devicebay and device_bays|length > 10 %}
|
||||
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for devicebay in device_bays %}
|
||||
@@ -355,19 +356,22 @@
|
||||
{% endif %}
|
||||
{% if interfaces or device.device_type.is_network_device %}
|
||||
{% if perms.dcim.delete_interface %}
|
||||
<form method="post" action="{% url 'dcim:interface_bulk_delete' pk=device.pk %}">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
{% if perms.dcim.add_interface and interfaces|length > 10 %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.add_interface and interfaces|length > 10 %}
|
||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for iface in interfaces %}
|
||||
@@ -380,8 +384,13 @@
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.delete_interface %}
|
||||
<div class="panel-footer">
|
||||
{% if interfaces and perms.dcim.change_interface %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.delete_interface %}
|
||||
<button type="submit" class="btn btn-danger btn-xs">
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -408,13 +417,16 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Ports</strong>
|
||||
{% if perms.dcim.add_consoleserverport and cs_ports|length > 10 %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.add_consoleserverport and cs_ports|length > 10 %}
|
||||
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for csp in cs_ports %}
|
||||
@@ -455,13 +467,16 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlets</strong>
|
||||
{% if perms.dcim.add_poweroutlet and power_outlets|length > 10 %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.add_poweroutlet and power_outlets|length > 10 %}
|
||||
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for po in power_outlets %}
|
||||
|
||||
60
netbox/templates/dcim/device_bulk_add_component.html
Normal file
60
netbox/templates/dcim/device_bulk_add_component.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Add {{ component_name|title }}</h1>
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if request.POST.redirect_url %}
|
||||
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
|
||||
{% endif %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Selected Devices</strong></div>
|
||||
<table class="panel-body table table-hover">
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>Type</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
{% for device in selected_devices %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||
<td>{{ device.device_type.full_name }}</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>{{ component_name|title }} to Add</strong></div>
|
||||
<div class="panel-body">
|
||||
{% for field in form.visible_fields %}
|
||||
{% render_field field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
<div class="col-md-12">
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -14,7 +14,7 @@
|
||||
{% for device in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
<td>{{ device.device_type.full_name }}</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.tenant }}</td>
|
||||
<td>{{ device.serial }}</td>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if module %}Editing {{ module.device }} {{ module }}{% else %}Add a Module to {{ device }}{% endif %}{% endblock %}
|
||||
{% block title %}Create {{ component_type }} ({{ device }}){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
@@ -18,13 +18,13 @@
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>{% if module %}Editing {{ module.device }} {{ module }}{% else %}Add a Module to {{ device }}{% endif %}</strong>
|
||||
<strong>{{ component_type }}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if module %}{{ module.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
<p class="form-control-static">{{ device }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
@@ -32,12 +32,8 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if module.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Position (U)</td>
|
||||
<td>Lowest rack unit occupied by the device (optional)</td>
|
||||
<td>Lowest-numbered rack unit occupied by the device (optional)</td>
|
||||
<td>21</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
<td>{{ device.device_type.full_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial Number</td>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if devicebay.pk %}Editing {{ devicebay.device }} {{ devicebay }}{% else %}Add a Device Bay ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if poweroutlet.pk %}
|
||||
<strong>Editing {{ devicebay }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Device Bay</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if devicebay %}{{ devicebay.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if devicebay.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{{ devicetype }}{% endblock %}
|
||||
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1>{{ devicetype }}</h1>
|
||||
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
@@ -72,6 +72,10 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Instances</td>
|
||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
@@ -143,14 +147,14 @@
|
||||
</div>
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' delete_url='dcim:devicetype_delete_interface' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if devicetype.is_parent_device %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
|
||||
{% endif %}
|
||||
{% if devicetype.is_network_device %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||
{% endif %}
|
||||
{% if devicetype.is_console_server %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ devicebay.installed_device.device_type }}</span>
|
||||
<span>{{ devicebay.installed_device.device_type.full_name }}</span>
|
||||
</td>
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<td>
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ ip.vrf|default:"Global" }}
|
||||
</td>
|
||||
<td>{{ ip.interface }}</td>
|
||||
<td>
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -28,10 +21,10 @@
|
||||
<ul class="rack rack_near_face">
|
||||
{% for u in primary_face %}
|
||||
{% if u.device %}
|
||||
<li class="occupied h{{ u.device.device_type.u_height }}u{% ifequal u.device.face face_id %} {{ u.device.device_role.color }}{% endifequal %}">
|
||||
<li class="occupied h{{ u.device.device_type.u_height }}u"{% ifequal u.device.face face_id %} style="background-color: #{{ u.device.device_role.color }}"{% endifequal %}>
|
||||
{% ifequal u.device.face face_id %}
|
||||
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
|
||||
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type }} ({{ u.device.device_type.u_height }}U)">
|
||||
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U)">
|
||||
{{ u.device.name|default:u.device.device_role }}
|
||||
{% if u.device.devicebay_count %}
|
||||
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})
|
||||
@@ -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 %}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block extra_actions %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_add_multi' %}" class="btn btn-primary btn-sm">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:device_bulk_add_interface' %}" class="btn btn-primary btn-sm">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Interfaces
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% if perms.dcim.change_devicetype %}
|
||||
<form method="post" action="{% url delete_url pk=devicetype.pk %}">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>{{ title }}</strong>
|
||||
{% if table.rows|length > 10 %}
|
||||
<div class="pull-right">
|
||||
<div class="pull-right">
|
||||
{% if table.rows|length > 3 %}
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if table.rows|length > 10 %}
|
||||
<a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add {{ title }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% render_table table 'table.html' %}
|
||||
<div class="panel-footer">
|
||||
{% if table.rows %}
|
||||
<button type="submit" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
{% if edit_url %}
|
||||
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}" class="btn btn-xs btn-warning">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if delete_url %}
|
||||
<button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs">
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Add Interfaces{% endblock %}
|
||||
|
||||
{% block selected_objects_title %}Selected Devices{% endblock %}
|
||||
|
||||
{% block form_title %}Interface(s) to Add{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>Type</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
{% for device in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
17
netbox/templates/dcim/interface_bulk_edit.html
Normal file
17
netbox/templates/dcim/interface_bulk_edit.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Interface Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Form Factor</th>
|
||||
</tr>
|
||||
{% for iface in selected_objects %}
|
||||
<tr>
|
||||
<td>{{ iface.name }}</td>
|
||||
<td>{{ iface.get_form_factor_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if interface.pk %}Editing {{ interface.device }} {{ interface }}{% else %}Add an Interface ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if interface.pk %}
|
||||
<strong>Editing {{ interface }}</strong>
|
||||
{% else %}
|
||||
<strong>Add an Interface</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if interface %}{{ interface.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if interface.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
25
netbox/templates/dcim/interfacetemplate_bulk_edit.html
Normal file
25
netbox/templates/dcim/interfacetemplate_bulk_edit.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Interface Template Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Form Factor</th>
|
||||
<th>Management</th>
|
||||
</tr>
|
||||
{% for iface in selected_objects %}
|
||||
<tr>
|
||||
<td>{{ iface.name }}</td>
|
||||
<td>{{ iface.get_form_factor_display }}</td>
|
||||
<td>
|
||||
{% if iface.mgmt_only %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Add an IP Address{% endblock %}
|
||||
{% block title %}Assign a New IP Address{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
@@ -18,10 +18,35 @@
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Add an IP Address
|
||||
<strong>IP Address</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% render_form form %}
|
||||
{% render_field form.address %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interface Assignment</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
{% render_field form.set_as_primary %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if poweroutlet.pk %}Editing {{ poweroutlet.device }} {{ poweroutlet }}{% else %}Add a Power Outlet ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if poweroutlet.pk %}
|
||||
<strong>Editing {{ poweroutlet }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Power Outlet</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if poweroutlet %}{{ poweroutlet.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if poweroutlet.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,51 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if powerport.pk %}Editing {{ powerport.device }} {{ powerport }}{% else %}Add a Power Port ({{ device }}){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% if powerport.pk %}
|
||||
<strong>Editing {{ powerport }}</strong>
|
||||
{% else %}
|
||||
<strong>Add a Power Port</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{% if powerport %}{{ powerport.device }}{% else %}{{ device }}{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
{% if powerport.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -122,7 +122,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Height</td>
|
||||
<td>{{ rack.u_height }}U</td>
|
||||
<td>{{ rack.u_height }}U ({% if rack.desc_units %}descending{% else %}ascending{% endif %})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Devices</td>
|
||||
@@ -153,8 +153,14 @@
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device.name }}</a>
|
||||
</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
<td>{% if device.parent_bay %}<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>{% endif %}</td>
|
||||
<td>{{ device.device_type.full_name }}</td>
|
||||
<td>
|
||||
{% if device.parent_bay %}
|
||||
<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -189,13 +195,13 @@
|
||||
<div class="rack_header">
|
||||
<h4>Front</h4>
|
||||
</div>
|
||||
{% include 'dcim/_rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %}
|
||||
{% include 'dcim/inc/_rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %}
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<h4>Rear</h4>
|
||||
</div>
|
||||
{% include 'dcim/_rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %}
|
||||
{% include 'dcim/inc/_rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
{% render_field form.type %}
|
||||
{% render_field form.width %}
|
||||
{% render_field form.u_height %}
|
||||
{% render_field form.desc_units %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
|
||||
@@ -73,10 +73,15 @@
|
||||
<td>Height in rack units</td>
|
||||
<td>42</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Descending units</td>
|
||||
<td>Units are numbered top-to-bottom</td>
|
||||
<td>False</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42</pre>
|
||||
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<tr>
|
||||
<td>{{ field }}</td>
|
||||
<td>
|
||||
{% if value == True %}
|
||||
{% if field.type == 300 and value == True %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="True"></i>
|
||||
{% elif value == False %}
|
||||
{% elif field.type == 300 and value == False %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="False"></i>
|
||||
{% elif field.type == 500 and value %}
|
||||
{{ value|urlizetrunc:75 }}
|
||||
{% elif value %}
|
||||
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
|
||||
{% elif field.type == 200 or value %}
|
||||
{{ value }}
|
||||
{% elif field.required %}
|
||||
<span class="text-warning">Not defined</span>
|
||||
|
||||
@@ -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>
|
||||
@@ -76,6 +76,12 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<span class="label label-{{ ipaddress.get_status_class }}">{{ ipaddress.get_status_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>
|
||||
@@ -91,8 +97,14 @@
|
||||
<td>
|
||||
{% if ipaddress.interface %}
|
||||
<span><a href="{% url 'dcim:device' pk=ipaddress.interface.device.pk %}">{{ ipaddress.interface.device }}</a> ({{ ipaddress.interface }})</span>
|
||||
{% if perms.dcim.change_device and perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_remove' pk=ipaddress.pk %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-remove"></i> Remove</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% if perms.dcim.change_device and perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_assign' pk=ipaddress.pk %}" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-plus"></i> Assign</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
72
netbox/templates/ipam/ipaddress_assign.html
Normal file
72
netbox/templates/ipam/ipaddress_assign.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Assign an IP Address{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<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>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="search">
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.device %}
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
{% render_field form.set_as_primary %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<button type="submit" name="_assign" class="btn btn-primary">Assign</button>
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/livesearch.js' %}"></script>
|
||||
{% endblock %}
|
||||
@@ -8,6 +8,7 @@
|
||||
<th>IP Address</th>
|
||||
<th>VRF</th>
|
||||
<th>Tenant</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
@@ -16,6 +17,7 @@
|
||||
<td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
|
||||
<td>{{ ipaddress.vrf|default:"Global" }}</td>
|
||||
<td>{{ ipaddress.tenant }}</td>
|
||||
<td>{{ ipaddress.get_status_display }}</td>
|
||||
<td>{% if ipaddress.interface %}<i class="glyphicon glyphicon-ok text-success" title="{{ ipaddress.interface.device }} {{ ipaddress.interface }}"></i>{% endif %}</td>
|
||||
<td>{{ ipaddress.description }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
{% render_field form.address %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% if obj %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Device</label>
|
||||
@@ -16,8 +17,12 @@
|
||||
<p class="form-control-static">
|
||||
{% if obj.interface %}
|
||||
<a href="{% url 'dcim:device' pk=obj.interface.device.pk %}">{{ obj.interface.device }}</a>
|
||||
<a href="{% url 'ipam:ipaddress_remove' pk=obj.pk %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-remove"></i> Remove</a>
|
||||
{% else %}
|
||||
<span>None</span>
|
||||
<span class="text-muted">None</span>
|
||||
{% if obj.pk %}
|
||||
<a href="{% url 'ipam:ipaddress_assign' pk=obj.pk %}" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-plus"></i> Assign</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
@@ -25,7 +30,13 @@
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Interface</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ obj.interface }}</p>
|
||||
<p class="form-control-static">
|
||||
{% if obj.interface %}
|
||||
{{ obj.interface }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -43,6 +43,11 @@
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>ABC01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>Device name (optional)</td>
|
||||
@@ -66,7 +71,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>192.0.2.42/24,65000:123,ABC01,switch12,ge-0/0/31,True,Management IP</pre>
|
||||
<pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
8
netbox/templates/ipam/ipaddress_unassign.html
Normal file
8
netbox/templates/ipam/ipaddress_unassign.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Remove {{ ipaddress }} from {{ ipaddress.interface }}?{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
<p>Are you sure you want to remove this IP address from <strong>{{ ipaddress.interface.device }} {{ ipaddress.interface }}</strong>?</p>
|
||||
{% endblock %}
|
||||
@@ -1,10 +1,22 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load humanize %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}RIRs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if request.GET.family == '6' %}
|
||||
<a href="{% url 'ipam:rir_list' %}" class="btn btn-default">
|
||||
<span class="fa fa-table" aria-hidden="true"></span>
|
||||
IPv4 Stats
|
||||
</a>
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_rir %}
|
||||
<a href="{% url 'ipam:rir_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
@@ -14,8 +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>
|
||||
{% 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>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
{% if request.POST.redirect_url %}
|
||||
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
|
||||
{% endif %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default">
|
||||
@@ -29,7 +32,13 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>{% block form_title %}Attributes{% endblock %}</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_form form %}
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name in form.nullable_fields %}
|
||||
{% render_field field bulk_nullable=True %}
|
||||
{% else %}
|
||||
{% render_field field %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
|
||||
@@ -5,26 +5,33 @@
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="checkbox{% if field.errors %} has-error{% endif %}">
|
||||
<label for="{{ field.id_for_label }}">
|
||||
{{ field }}
|
||||
{{ field.label }}
|
||||
{{ field }} {{ field.label }}
|
||||
</label>
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif field|widget_type == 'radioselect' %}
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="radio{% if field.errors %} has-error{% endif %}">
|
||||
<label for="{{ field.id_for_label }}">
|
||||
{{ field }}
|
||||
{{ field.label }}
|
||||
{% if bulk_nullable %}
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<ul>
|
||||
{% for error in field.errors %}
|
||||
<li class="text-danger">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif field|widget_type == 'textarea' %}
|
||||
<div class="col-md-12">
|
||||
{{ field }}
|
||||
{% if bulk_nullable %}
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
@@ -40,6 +47,11 @@
|
||||
<label class="col-md-3 control-label{% if field.field.required %} required{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
<div class="col-md-9">
|
||||
{{ field }}
|
||||
{% if bulk_nullable %}
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -7,30 +7,6 @@ from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDat
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
def bulkedit_tenantgroup_choices():
|
||||
"""
|
||||
Include an option to remove the currently assigned TenantGroup from a Tenant.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(g.pk, g.name) for g in TenantGroup.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
def bulkedit_tenant_choices():
|
||||
"""
|
||||
Include an option to remove the currently assigned Tenant from an object.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(t.pk, t.name) for t in Tenant.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
#
|
||||
# Tenant groups
|
||||
#
|
||||
@@ -71,7 +47,10 @@ class TenantImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group')
|
||||
group = forms.ModelChoiceField(queryset=TenantGroup.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['group']
|
||||
|
||||
|
||||
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
|
||||
@@ -28,8 +28,8 @@ class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'tenancy.change_tenantgroup'
|
||||
model = TenantGroup
|
||||
form_class = forms.TenantGroupForm
|
||||
success_url = 'tenancy:tenantgroup_list'
|
||||
cancel_url = 'tenancy:tenantgroup_list'
|
||||
obj_list_url = 'tenancy:tenantgroup_list'
|
||||
use_obj_view = False
|
||||
|
||||
|
||||
class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@@ -83,7 +83,7 @@ class TenantEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
form_class = forms.TenantForm
|
||||
fields_initial = ['group']
|
||||
template_name = 'tenancy/tenant_edit.html'
|
||||
cancel_url = 'tenancy:tenant_list'
|
||||
obj_list_url = 'tenancy:tenant_list'
|
||||
|
||||
|
||||
class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render, resolve_url
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.http import is_safe_url
|
||||
|
||||
from secrets.forms import UserKeyForm
|
||||
@@ -26,11 +25,11 @@ def login(request):
|
||||
# Determine where to direct user after successful login
|
||||
redirect_to = request.POST.get('next', '')
|
||||
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
||||
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
|
||||
redirect_to = reverse('home')
|
||||
|
||||
# Authenticate user
|
||||
auth_login(request, form.get_user())
|
||||
messages.info(request, "Logged in as {0}.".format(request.user))
|
||||
messages.info(request, u"Logged in as {}.".format(request.user))
|
||||
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
@@ -45,7 +44,7 @@ def login(request):
|
||||
def logout(request):
|
||||
|
||||
auth_logout(request)
|
||||
messages.info(request, "You have logged out.")
|
||||
messages.info(request, u"You have logged out.")
|
||||
return HttpResponseRedirect(reverse('home'))
|
||||
|
||||
|
||||
@@ -68,7 +67,7 @@ def change_password(request):
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
update_session_auth_hash(request, form.user)
|
||||
messages.success(request, "Your password has been changed successfully.")
|
||||
messages.success(request, u"Your password has been changed successfully.")
|
||||
return redirect('users:profile')
|
||||
|
||||
else:
|
||||
@@ -106,7 +105,7 @@ def userkey_edit(request):
|
||||
uk = form.save(commit=False)
|
||||
uk.user = request.user
|
||||
uk.save()
|
||||
messages.success(request, "Your user key has been saved.")
|
||||
messages.success(request, u"Your user key has been saved.")
|
||||
return redirect('users:userkey')
|
||||
|
||||
else:
|
||||
|
||||
@@ -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,30 +11,82 @@ from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
EXPANSION_PATTERN = '\[(\d+-\d+)\]'
|
||||
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})\]'
|
||||
|
||||
|
||||
def expand_pattern(string):
|
||||
def expand_numeric_pattern(string):
|
||||
"""
|
||||
Expand a numeric pattern into a list of strings. Examples:
|
||||
'ge-0/0/[0-3]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3']
|
||||
'xe-0/[0-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
|
||||
"""
|
||||
lead, pattern, remnant = re.split(EXPANSION_PATTERN, string, maxsplit=1)
|
||||
lead, pattern, remnant = re.split(NUMERIC_EXPANSION_PATTERN, string, maxsplit=1)
|
||||
x, y = pattern.split('-')
|
||||
for i in range(int(x), int(y) + 1):
|
||||
if re.search(EXPANSION_PATTERN, remnant):
|
||||
for string in expand_pattern(remnant):
|
||||
if re.search(NUMERIC_EXPANSION_PATTERN, remnant):
|
||||
for string in expand_numeric_pattern(remnant):
|
||||
yield "{}{}{}".format(lead, i, string)
|
||||
else:
|
||||
yield "{}{}{}".format(lead, i, remnant)
|
||||
|
||||
|
||||
def expand_ipaddress_pattern(string, family):
|
||||
"""
|
||||
Expand an IP address pattern into a list of strings. Examples:
|
||||
'192.0.2.[1-254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.3/24' ... '192.0.2.254/24']
|
||||
'2001:db8:0:[0-ff]::/64' => ['2001:db8:0:0::/64', '2001:db8:0:1::/64', ... '2001:db8:0:ff::/64']
|
||||
"""
|
||||
if family not in [4, 6]:
|
||||
raise Exception("Invalid IP address family: {}".format(family))
|
||||
if family == 4:
|
||||
regex = IP4_EXPANSION_PATTERN
|
||||
base = 10
|
||||
else:
|
||||
regex = IP6_EXPANSION_PATTERN
|
||||
base = 16
|
||||
lead, pattern, remnant = re.split(regex, string, maxsplit=1)
|
||||
x, y = pattern.split('-')
|
||||
for i in range(int(x, base), int(y, base) + 1):
|
||||
if re.search(regex, remnant):
|
||||
for string in expand_ipaddress_pattern(remnant, family):
|
||||
yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), string])
|
||||
else:
|
||||
yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant])
|
||||
|
||||
|
||||
def add_blank_choice(choices):
|
||||
"""
|
||||
Add a blank choice to the beginning of a choices list.
|
||||
"""
|
||||
return ((None, '---------'),) + choices
|
||||
return ((None, '---------'),) + tuple(choices)
|
||||
|
||||
|
||||
#
|
||||
@@ -45,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
|
||||
@@ -178,8 +251,28 @@ class ExpandableNameField(forms.CharField):
|
||||
'Example: <code>ge-0/0/[0-47]</code>'
|
||||
|
||||
def to_python(self, value):
|
||||
if re.search(EXPANSION_PATTERN, value):
|
||||
return list(expand_pattern(value))
|
||||
if re.search(NUMERIC_EXPANSION_PATTERN, value):
|
||||
return list(expand_numeric_pattern(value))
|
||||
return [value]
|
||||
|
||||
|
||||
class ExpandableIPAddressField(forms.CharField):
|
||||
"""
|
||||
A field which allows for expansion of IP address ranges
|
||||
Example: '192.0.2.[1-254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.3/24' ... '192.0.2.254/24']
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ExpandableIPAddressField, self).__init__(*args, **kwargs)
|
||||
if not self.help_text:
|
||||
self.help_text = 'Specify a numeric range to create multiple IPs.<br />'\
|
||||
'Example: <code>192.0.2.[1-254]/24</code>'
|
||||
|
||||
def to_python(self, value):
|
||||
# Hackish address family detection but it's all we have to work with
|
||||
if '.' in value and re.search(IP4_EXPANSION_PATTERN, value):
|
||||
return list(expand_ipaddress_pattern(value, 4))
|
||||
elif ':' in value and re.search(IP6_EXPANSION_PATTERN, value):
|
||||
return list(expand_ipaddress_pattern(value, 6))
|
||||
return [value]
|
||||
|
||||
|
||||
@@ -188,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">'\
|
||||
@@ -195,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):
|
||||
@@ -294,6 +389,18 @@ class ConfirmationForm(forms.Form, BootstrapMixin):
|
||||
confirm = forms.BooleanField(required=True)
|
||||
|
||||
|
||||
class BulkEditForm(forms.Form):
|
||||
|
||||
def __init__(self, model, *args, **kwargs):
|
||||
super(BulkEditForm, self).__init__(*args, **kwargs)
|
||||
self.model = model
|
||||
# Copy any nullable fields defined in Meta
|
||||
if hasattr(self.Meta, 'nullable_fields'):
|
||||
self.nullable_fields = [field for field in self.Meta.nullable_fields]
|
||||
else:
|
||||
self.nullable_fields = []
|
||||
|
||||
|
||||
class BulkImportForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user