mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-27 23:57:46 -06:00
Compare commits
115 Commits
v2.0-beta2
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1bcaa33e7 | ||
|
|
a35f8bddde | ||
|
|
8fbe7ba742 | ||
|
|
f039b0b6e9 | ||
|
|
9ad9ef7957 | ||
|
|
5c7db04465 | ||
|
|
838105fb65 | ||
|
|
5ca87c0f20 | ||
|
|
af4edff370 | ||
|
|
f40c048475 | ||
|
|
77247cccbe | ||
|
|
fcfcd77bfd | ||
|
|
b3667befb4 | ||
|
|
a6cb0e0a96 | ||
|
|
c047f943de | ||
|
|
79089cc47e | ||
|
|
3c631902e1 | ||
|
|
379c24a012 | ||
|
|
4035b87693 | ||
|
|
11d1a8c3cf | ||
|
|
7eb9c8265c | ||
|
|
572beb2311 | ||
|
|
d861d8bfb8 | ||
|
|
6791ff6192 | ||
|
|
9d9de6b2a3 | ||
|
|
1f7ef15ad1 | ||
|
|
16c582ec7a | ||
|
|
de58d0ecca | ||
|
|
010f6c7f1a | ||
|
|
aea5612c39 | ||
|
|
b8b912bdd5 | ||
|
|
e4ca88726e | ||
|
|
616f109671 | ||
|
|
8e0580ff96 | ||
|
|
4b2e7620dd | ||
|
|
b82f25c503 | ||
|
|
c174c0cc6d | ||
|
|
117da337c7 | ||
|
|
01da46f753 | ||
|
|
d17efce4f5 | ||
|
|
e7a6d1f532 | ||
|
|
f643f2c601 | ||
|
|
480faa6461 | ||
|
|
1fa084b6be | ||
|
|
1c86b00b5c | ||
|
|
10823e1c37 | ||
|
|
f73693206f | ||
|
|
861c8b29c0 | ||
|
|
17873706b7 | ||
|
|
5037046624 | ||
|
|
5c0614d656 | ||
|
|
697866d1ba | ||
|
|
38d826d152 | ||
|
|
13cc29cd8c | ||
|
|
401357b8cb | ||
|
|
599e1bb220 | ||
|
|
864fa17b75 | ||
|
|
a98c9ed0af | ||
|
|
8032aa1ad9 | ||
|
|
b01bf6089c | ||
|
|
f9a33bfc14 | ||
|
|
610b412506 | ||
|
|
09000ad9b3 | ||
|
|
f70f0f8d62 | ||
|
|
d5c3f9e780 | ||
|
|
b42dab3eef | ||
|
|
7cbea49c2d | ||
|
|
6dcc5a1169 | ||
|
|
53129125dd | ||
|
|
2d52b9fb39 | ||
|
|
863cbb785d | ||
|
|
ba1a4f06ff | ||
|
|
cf5be85dad | ||
|
|
d21b67446f | ||
|
|
3b48a270fc | ||
|
|
105e9da866 | ||
|
|
d3b16ba443 | ||
|
|
57fc6a3f50 | ||
|
|
abc51fdc5d | ||
|
|
e0ad2b4555 | ||
|
|
35a0a658a7 | ||
|
|
2c99a8bee4 | ||
|
|
1dd2bdcb8e | ||
|
|
9f67da00d1 | ||
|
|
82d53a8c3d | ||
|
|
f3eee25527 | ||
|
|
ee11775425 | ||
|
|
bcdf9ac5ca | ||
|
|
4accdf77f8 | ||
|
|
fc46f70153 | ||
|
|
e7cf7d58b8 | ||
|
|
d98e9e1838 | ||
|
|
369d3aa62e | ||
|
|
d4ac6dbfe4 | ||
|
|
91d35905fd | ||
|
|
78b0072051 | ||
|
|
7766e1f684 | ||
|
|
f89d91783b | ||
|
|
3ffe36e5ed | ||
|
|
be393a9d10 | ||
|
|
27eefd8705 | ||
|
|
097e0f38ff | ||
|
|
ce26b566a4 | ||
|
|
0e14bc1e02 | ||
|
|
ce6796ed9b | ||
|
|
c90cecc2fb | ||
|
|
b6bbcb0609 | ||
|
|
23f6832d9c | ||
|
|
88dace75a1 | ||
|
|
8eb140fd65 | ||
|
|
1f09f3d096 | ||
|
|
66be85a41f | ||
|
|
814c11167e | ||
|
|
57ddd5086f | ||
|
|
c171547037 |
20
Dockerfile
20
Dockerfile
@@ -1,20 +0,0 @@
|
||||
FROM python:2.7-wheezy
|
||||
|
||||
WORKDIR /opt/netbox
|
||||
|
||||
ARG BRANCH=v2.0-beta1
|
||||
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 graphviz && \
|
||||
pip install gunicorn==17.5 && \
|
||||
pip install django-auth-ldap && \
|
||||
pip install -r requirements.txt
|
||||
|
||||
ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
ADD netbox/netbox/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
|
||||
|
||||
ENTRYPOINT [ "/docker-entrypoint.sh" ]
|
||||
|
||||
ADD docker/gunicorn_config.py /opt/netbox/
|
||||
ADD docker/nginx.conf /etc/netbox-nginx/
|
||||
VOLUME ["/etc/netbox-nginx/"]
|
||||
@@ -10,7 +10,9 @@ Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https
|
||||
|
||||
### Build Status
|
||||
|
||||
| | python 2.7 |
|
||||
NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
|
||||
|
||||
| | status |
|
||||
|-------------|------------|
|
||||
| **master** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
| **develop** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
@@ -29,5 +31,5 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst
|
||||
|
||||
## Alternative Installations
|
||||
|
||||
* [Docker container](http://netbox.readthedocs.io/en/stable/installation/docker/)
|
||||
* [Docker container](https://github.com/digitalocean/netbox-docker)
|
||||
* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:9.6
|
||||
container_name: postgres
|
||||
environment:
|
||||
POSTGRES_USER: netbox
|
||||
POSTGRES_PASSWORD: J5brHrAXFLQSif0K
|
||||
POSTGRES_DB: netbox
|
||||
netbox:
|
||||
build: .
|
||||
image: digitalocean/netbox
|
||||
links:
|
||||
- postgres
|
||||
container_name: netbox
|
||||
depends_on:
|
||||
- postgres
|
||||
environment:
|
||||
SUPERUSER_NAME: admin
|
||||
SUPERUSER_EMAIL: admin@example.com
|
||||
SUPERUSER_PASSWORD: admin
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: netbox
|
||||
DB_USER: netbox
|
||||
DB_PASSWORD: J5brHrAXFLQSif0K
|
||||
DB_HOST: postgres
|
||||
SECRET_KEY: r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
|
||||
EMAIL_SERVER: localhost
|
||||
EMAIL_PORT: 25
|
||||
EMAIL_USERNAME: foo
|
||||
EMAIL_PASSWORD: bar
|
||||
EMAIL_TIMEOUT: 10
|
||||
EMAIL_FROM: netbox@bar.com
|
||||
NETBOX_USERNAME: guest
|
||||
NETBOX_PASSWORD: guest
|
||||
volumes:
|
||||
- netbox-static-files:/opt/netbox/netbox/static
|
||||
nginx:
|
||||
image: nginx:1.11.1-alpine
|
||||
links:
|
||||
- netbox
|
||||
container_name: nginx
|
||||
command: nginx -g 'daemon off;' -c /etc/netbox-nginx/nginx.conf
|
||||
depends_on:
|
||||
- netbox
|
||||
ports:
|
||||
- 80:80
|
||||
volumes_from:
|
||||
- netbox
|
||||
volumes:
|
||||
netbox-static-files:
|
||||
driver: local
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# run db migrations (retry on error)
|
||||
while ! /opt/netbox/netbox/manage.py migrate 2>&1; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# create superuser silently
|
||||
if [[ -z ${SUPERUSER_NAME} || -z ${SUPERUSER_EMAIL} || -z ${SUPERUSER_PASSWORD} ]]; then
|
||||
SUPERUSER_NAME='admin'
|
||||
SUPERUSER_EMAIL='admin@example.com'
|
||||
SUPERUSER_PASSWORD='admin'
|
||||
echo "Using defaults: Username: ${SUPERUSER_NAME}, E-Mail: ${SUPERUSER_EMAIL}, Password: ${SUPERUSER_PASSWORD}"
|
||||
fi
|
||||
echo "from django.contrib.auth.models import User; User.objects.create_superuser('${SUPERUSER_NAME}', '${SUPERUSER_EMAIL}', '${SUPERUSER_PASSWORD}')" | python /opt/netbox/netbox/manage.py shell
|
||||
|
||||
# copy static files
|
||||
/opt/netbox/netbox/manage.py collectstatic --no-input
|
||||
|
||||
# start unicorn
|
||||
gunicorn --log-level debug --debug --error-logfile /dev/stderr --log-file /dev/stdout -c /opt/netbox/gunicorn_config.py netbox.wsgi
|
||||
@@ -1,5 +0,0 @@
|
||||
command = '/usr/bin/gunicorn'
|
||||
pythonpath = '/opt/netbox/netbox'
|
||||
bind = '0.0.0.0:8001'
|
||||
workers = 3
|
||||
user = 'root'
|
||||
@@ -1,35 +0,0 @@
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
server_tokens off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
server_name localhost;
|
||||
|
||||
access_log off;
|
||||
|
||||
location /static/ {
|
||||
alias /opt/netbox/netbox/static/;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://netbox:8001;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
This guide demonstrates how to build and run NetBox as a Docker container. It assumes that the latest versions of [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) are already installed in your host.
|
||||
|
||||
# Quickstart
|
||||
|
||||
To get NetBox up and running:
|
||||
|
||||
```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.
|
||||
|
||||
Default credentials:
|
||||
|
||||
* Username: **admin**
|
||||
* Password: **admin**
|
||||
|
||||
# Configuration
|
||||
|
||||
You can configure the app at runtime using variables (see `docker-compose.yml`). Possible environment variables include:
|
||||
|
||||
* SUPERUSER_NAME
|
||||
* SUPERUSER_EMAIL
|
||||
* SUPERUSER_PASSWORD
|
||||
* ALLOWED_HOSTS
|
||||
* DB_NAME
|
||||
* DB_USER
|
||||
* DB_PASSWORD
|
||||
* DB_HOST
|
||||
* DB_PORT
|
||||
* SECRET_KEY
|
||||
* EMAIL_SERVER
|
||||
* EMAIL_PORT
|
||||
* EMAIL_USERNAME
|
||||
* EMAIL_PASSWORD
|
||||
* EMAIL_TIMEOUT
|
||||
* EMAIL_FROM
|
||||
* LOGIN_REQUIRED
|
||||
* MAINTENANCE_MODE
|
||||
* NETBOX_USERNAME
|
||||
* NETBOX_PASSWORD
|
||||
* PAGINATE_COUNT
|
||||
* TIME_ZONE
|
||||
* DATE_FORMAT
|
||||
* SHORT_DATE_FORMAT
|
||||
* TIME_FORMAT
|
||||
* SHORT_TIME_FORMAT
|
||||
* DATETIME_FORMAT
|
||||
* SHORT_DATETIME_FORMAT
|
||||
@@ -6,6 +6,7 @@ Python 3:
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
|
||||
# update-alternatives --install /usr/bin/python python /usr/bin/python3 1
|
||||
```
|
||||
|
||||
Python 2:
|
||||
@@ -20,7 +21,9 @@ Python 3:
|
||||
|
||||
```no-highlight
|
||||
# yum install -y epel-release
|
||||
# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
|
||||
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
|
||||
# easy_install-3.4 pip
|
||||
# ln -s -f python3.4 /usr/bin/python
|
||||
```
|
||||
|
||||
Python 2:
|
||||
@@ -83,6 +86,14 @@ 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.)
|
||||
|
||||
Python 3:
|
||||
|
||||
```no-highlight
|
||||
# pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
Python 2:
|
||||
|
||||
```no-highlight
|
||||
# pip install -r requirements.txt
|
||||
```
|
||||
@@ -172,7 +183,7 @@ Superuser created successfully.
|
||||
# Collect Static Files
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py collectstatic
|
||||
# ./manage.py collectstatic --no-input
|
||||
|
||||
You have requested to collect static files at the destination
|
||||
location as specified in your settings:
|
||||
|
||||
@@ -5,13 +5,14 @@ NetBox requires a PostgreSQL database to store data. (Please note that MySQL is
|
||||
**Debian/Ubuntu**
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y postgresql libpq-dev python-psycopg2
|
||||
# apt-get update
|
||||
# apt-get install -y postgresql libpq-dev
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
|
||||
```no-highlight
|
||||
# yum install -y postgresql postgresql-server postgresql-devel python-psycopg2
|
||||
# yum install -y postgresql postgresql-server postgresql-devel
|
||||
# postgresql-setup initdb
|
||||
```
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ pages:
|
||||
- 'Web Server': 'installation/web-server.md'
|
||||
- 'LDAP (Optional)': 'installation/ldap.md'
|
||||
- 'Upgrading': 'installation/upgrading.md'
|
||||
- 'Alternate Install: Docker': 'installation/docker.md'
|
||||
- 'Configuration':
|
||||
- 'Mandatory Settings': 'configuration/mandatory-settings.md'
|
||||
- 'Optional Settings': 'configuration/optional-settings.md'
|
||||
|
||||
@@ -28,11 +28,14 @@ class NestedProviderSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class WritableProviderSerializer(serializers.ModelSerializer):
|
||||
class WritableProviderSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
'custom_fields',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
@@ -79,11 +82,14 @@ class NestedCircuitSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'cid']
|
||||
|
||||
|
||||
class WritableCircuitSerializer(serializers.ModelSerializer):
|
||||
class WritableCircuitSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||
fields = [
|
||||
'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
'custom_fields',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -22,4 +22,5 @@ router.register(r'circuit-types', views.CircuitTypeViewSet)
|
||||
router.register(r'circuits', views.CircuitViewSet)
|
||||
router.register(r'circuit-terminations', views.CircuitTerminationViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-04-19 17:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0007_circuit_add_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='interface',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface'),
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from dcim.fields import ASNField
|
||||
@@ -150,10 +150,14 @@ class CircuitTermination(models.Model):
|
||||
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
|
||||
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
|
||||
site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT)
|
||||
interface = models.OneToOneField('dcim.Interface', related_name='circuit_termination', blank=True, null=True)
|
||||
interface = models.OneToOneField(
|
||||
'dcim.Interface', related_name='circuit_termination', blank=True, null=True, on_delete=models.PROTECT
|
||||
)
|
||||
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
|
||||
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
||||
help_text='Upstream speed, if different from port speed')
|
||||
upstream_speed = models.PositiveIntegerField(
|
||||
blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
||||
help_text='Upstream speed, if different from port speed'
|
||||
)
|
||||
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
|
||||
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
|
||||
# Providers
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from utilities.forms import ConfirmationForm
|
||||
@@ -95,7 +95,7 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = CircuitType
|
||||
form_class = forms.CircuitTypeForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('circuits:circuittype_list')
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
model = Circuit
|
||||
form_class = forms.CircuitForm
|
||||
fields_initial = ['provider']
|
||||
template_name = 'circuits/circuit_edit.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
@@ -230,7 +229,6 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuittermination'
|
||||
model = CircuitTermination
|
||||
form_class = forms.CircuitTerminationForm
|
||||
fields_initial = ['term_side']
|
||||
template_name = 'circuits/circuittermination_edit.html'
|
||||
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
@@ -238,7 +236,7 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.circuit.get_absolute_url()
|
||||
|
||||
|
||||
|
||||
@@ -66,13 +66,13 @@ class NestedSiteSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class WritableSiteSerializer(serializers.ModelSerializer):
|
||||
class WritableSiteSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||
'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields',
|
||||
]
|
||||
|
||||
|
||||
@@ -150,13 +150,13 @@ class NestedRackSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'name', 'display_name']
|
||||
|
||||
|
||||
class WritableRackSerializer(serializers.ModelSerializer):
|
||||
class WritableRackSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
||||
'comments',
|
||||
'comments', 'custom_fields',
|
||||
]
|
||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
|
||||
# prevents facility_id from being interpreted as a required field.
|
||||
@@ -263,13 +263,13 @@ class NestedDeviceTypeSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
|
||||
|
||||
|
||||
class WritableDeviceTypeSerializer(serializers.ModelSerializer):
|
||||
class WritableDeviceTypeSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments',
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
||||
]
|
||||
|
||||
|
||||
@@ -476,13 +476,13 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
class WritableDeviceSerializer(serializers.ModelSerializer):
|
||||
class WritableDeviceSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack',
|
||||
'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'comments',
|
||||
'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'comments', 'custom_fields',
|
||||
]
|
||||
validators = []
|
||||
|
||||
@@ -490,7 +490,7 @@ class WritableDeviceSerializer(serializers.ModelSerializer):
|
||||
|
||||
# Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta.
|
||||
if data.get('rack') and data.get('position') and data.get('face'):
|
||||
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('rack', 'position', 'face'))
|
||||
validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face'))
|
||||
validator.set_context(self)
|
||||
validator(data)
|
||||
|
||||
|
||||
@@ -58,4 +58,5 @@ router.register(r'interface-connections', views.InterfaceConnectionViewSet)
|
||||
# Miscellaneous
|
||||
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
|
||||
|
||||
app_name = 'dcim-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -2,16 +2,16 @@ from rest_framework.decorators import detail_route
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet, ViewSet
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
|
||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||
RackRole, Region, Site,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site,
|
||||
)
|
||||
from dcim import filters
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
@@ -323,6 +323,7 @@ class InterfaceConnectionViewSet(WritableSerializerMixin, ModelViewSet):
|
||||
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
write_serializer_class = serializers.WritableInterfaceConnectionSerializer
|
||||
filter_class = filters.InterfaceConnectionFilter
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -8,9 +8,9 @@ from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceTemplate, Manufacturer, InventoryItem,
|
||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||
RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
|
||||
)
|
||||
|
||||
|
||||
@@ -148,6 +148,33 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class RackReservationFilter(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = NullableModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -158,6 +185,16 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
model = RackReservation
|
||||
fields = ['rack', 'user']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(rack__name__icontains=value) |
|
||||
Q(rack__facility_id__icontains=value) |
|
||||
Q(user__username__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
@@ -336,10 +373,6 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
status = django_filters.BooleanFilter(
|
||||
name='status',
|
||||
label='Status',
|
||||
)
|
||||
is_console_server = django_filters.BooleanFilter(
|
||||
name='device_type__is_console_server',
|
||||
label='Is a console server',
|
||||
@@ -356,6 +389,9 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='_has_primary_ip',
|
||||
label='Has a primary IP',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=STATUS_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
@@ -401,7 +437,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
name='device__name',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
@@ -550,6 +586,10 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
label='Device',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['connection_status']
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import re
|
||||
|
||||
from mptt.forms import TreeNodeChoiceField
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||
@@ -11,9 +10,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
|
||||
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
|
||||
SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
|
||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||
BulkImportForm, CommentField, CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField,
|
||||
Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
|
||||
)
|
||||
|
||||
from .formfields import MACAddressFormField
|
||||
@@ -21,9 +20,9 @@ from .models import (
|
||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
||||
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
||||
SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate,
|
||||
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES,
|
||||
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES,
|
||||
)
|
||||
|
||||
|
||||
@@ -272,6 +271,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
|
||||
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
|
||||
u_height = forms.IntegerField(required=False, label='Height (U)')
|
||||
desc_units = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Descending units')
|
||||
comments = CommentField(widget=SmallTextarea)
|
||||
|
||||
class Meta:
|
||||
@@ -330,6 +330,19 @@ class RackReservationForm(BootstrapMixin, forms.ModelForm):
|
||||
return unit_choices
|
||||
|
||||
|
||||
class RackReservationFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('racks__reservations')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
group_id = FilterChoiceField(
|
||||
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
|
||||
label='Rack group',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@@ -362,7 +375,13 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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)
|
||||
is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
|
||||
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
|
||||
is_console_server = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
|
||||
is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
|
||||
is_network_device = forms.NullBooleanField(
|
||||
required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
@@ -471,6 +490,7 @@ class InterfaceTemplateCreateForm(DeviceComponentForm):
|
||||
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)
|
||||
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
@@ -520,27 +540,32 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'position'}
|
||||
))
|
||||
position = forms.TypedChoiceField(required=False, empty_value=None,
|
||||
help_text="The lowest-numbered unit occupied by the device",
|
||||
widget=APISelect(api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}',
|
||||
disabled_indicator='device'))
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
|
||||
widget=forms.Select(attrs={'filter-for': 'device_type'}))
|
||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Device type', widget=APISelect(
|
||||
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
|
||||
display_field='model'
|
||||
))
|
||||
rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(), required=False, widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'position'}
|
||||
)
|
||||
)
|
||||
position = forms.TypedChoiceField(
|
||||
required=False, empty_value=None, help_text="The lowest-numbered unit occupied by the device",
|
||||
widget=APISelect(api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}', disabled_indicator='device')
|
||||
)
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(), widget=forms.Select(attrs={'filter-for': 'device_type'})
|
||||
)
|
||||
device_type = forms.ModelChoiceField(
|
||||
queryset=DeviceType.objects.all(), label='Device type',
|
||||
widget=APISelect(api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}', display_field='model')
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position',
|
||||
'face', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'comments']
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
|
||||
'status', 'platform', 'primary_ip4', 'primary_ip6', 'comments',
|
||||
]
|
||||
help_texts = {
|
||||
'device_role': "The function this device serves",
|
||||
'serial': "Chassis serial number",
|
||||
@@ -629,15 +654,24 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
|
||||
class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid device role.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid manufacturer.'})
|
||||
device_role = forms.ModelChoiceField(
|
||||
queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid device role.'}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'}
|
||||
)
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid manufacturer.'}
|
||||
)
|
||||
model_name = forms.CharField()
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid platform.'})
|
||||
platform = forms.ModelChoiceField(
|
||||
queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid platform.'}
|
||||
)
|
||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in STATUS_CHOICES])
|
||||
|
||||
class Meta:
|
||||
fields = []
|
||||
@@ -655,17 +689,24 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||
except DeviceType.DoesNotExist:
|
||||
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
|
||||
|
||||
def clean_status_name(self):
|
||||
return dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
|
||||
|
||||
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
})
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
}
|
||||
)
|
||||
rack_name = forms.CharField(required=False)
|
||||
face = forms.CharField(required=False)
|
||||
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'rack_name', 'position', 'face']
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
|
||||
'status_name', 'site', 'rack_name', 'position', 'face',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -707,8 +748,8 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'parent',
|
||||
'device_bay_name',
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
|
||||
'status_name', 'parent', 'device_bay_name',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
@@ -752,6 +793,13 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
nullable_fields = ['tenant', 'platform']
|
||||
|
||||
|
||||
def device_status_choices():
|
||||
status_counts = {}
|
||||
for status in Device.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 STATUS_CHOICES]
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Device
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
@@ -771,10 +819,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
|
||||
null_option=(0, 'None'),
|
||||
)
|
||||
manufacturer_id = FilterChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer',
|
||||
)
|
||||
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
|
||||
device_type_id = FilterChoiceField(
|
||||
queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
|
||||
filter_count=Count('instances'),
|
||||
@@ -786,14 +831,8 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
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',
|
||||
)
|
||||
status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
|
||||
mac_address = forms.CharField(required=False, label='MAC address')
|
||||
|
||||
|
||||
#
|
||||
@@ -1400,6 +1439,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
|
||||
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -1409,9 +1449,16 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit LAG choices to interfaces which belong to the parent device.
|
||||
device = None
|
||||
if self.initial.get('device'):
|
||||
self.fields['lag'].queryset = Interface.objects.filter(
|
||||
device=self.initial['device'], form_factor=IFACE_FF_LAG
|
||||
try:
|
||||
device = Device.objects.get(pk=self.initial.get('device'))
|
||||
except Device.DoesNotExist:
|
||||
pass
|
||||
if device is not None:
|
||||
interface_ordering = device.device_type.interface_ordering
|
||||
self.fields['lag'].queryset = Interface.objects.order_naturally(method=interface_ordering).filter(
|
||||
device=device, form_factor=IFACE_FF_LAG
|
||||
)
|
||||
else:
|
||||
self.fields['lag'].choices = []
|
||||
@@ -1671,36 +1718,6 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
device = forms.CharField(required=False, label='Device name')
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
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', 'tenant', 'status', 'interface', 'description']
|
||||
|
||||
def __init__(self, device, *args, **kwargs):
|
||||
|
||||
super(IPAddressForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
interfaces = device.interfaces.all()
|
||||
self.fields['interface'].queryset = interfaces
|
||||
self.fields['interface'].required = True
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
#
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
27
netbox/dcim/migrations/0035_device_expand_status_choices.py
Normal file
27
netbox/dcim/migrations/0035_device_expand_status_choices.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-08 15:57
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0034_rename_module_to_inventoryitem'),
|
||||
]
|
||||
|
||||
# We convert the BooleanField to an IntegerField first as PostgreSQL does not provide a direct cast for boolean to
|
||||
# smallint (attempting to convert directly yields the error "cannot cast type boolean to smallint").
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
]
|
||||
25
netbox/dcim/migrations/0036_add_ff_juniper_vcp.py
Normal file
25
netbox/dcim/migrations/0036_add_ff_juniper_vcp.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-09 16:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0035_device_expand_status_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [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)'], [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'], [5200, b'Juniper VCP']]], [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'], [200, b'Link Aggregation Group (LAG)']]], [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)'], [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'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
from collections import OrderedDict
|
||||
from itertools import count, groupby
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
@@ -8,10 +9,10 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from circuits.models import Circuit
|
||||
@@ -101,6 +102,7 @@ IFACE_FF_STACKWISE = 5000
|
||||
IFACE_FF_STACKWISE_PLUS = 5050
|
||||
IFACE_FF_FLEXSTACK = 5100
|
||||
IFACE_FF_FLEXSTACK_PLUS = 5150
|
||||
IFACE_FF_JUNIPER_VCP = 5200
|
||||
# Other
|
||||
IFACE_FF_OTHER = 32767
|
||||
|
||||
@@ -162,6 +164,7 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
||||
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -177,13 +180,30 @@ VIRTUAL_IFACE_TYPES = [
|
||||
IFACE_FF_LAG,
|
||||
]
|
||||
|
||||
STATUS_ACTIVE = True
|
||||
STATUS_OFFLINE = False
|
||||
STATUS_OFFLINE = 0
|
||||
STATUS_ACTIVE = 1
|
||||
STATUS_PLANNED = 2
|
||||
STATUS_STAGED = 3
|
||||
STATUS_FAILED = 4
|
||||
STATUS_INVENTORY = 5
|
||||
STATUS_CHOICES = [
|
||||
[STATUS_ACTIVE, 'Active'],
|
||||
[STATUS_OFFLINE, 'Offline'],
|
||||
[STATUS_PLANNED, 'Planned'],
|
||||
[STATUS_STAGED, 'Staged'],
|
||||
[STATUS_FAILED, 'Failed'],
|
||||
[STATUS_INVENTORY, 'Inventory'],
|
||||
]
|
||||
|
||||
DEVICE_STATUS_CLASSES = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
3: 'primary',
|
||||
4: 'danger',
|
||||
5: 'default',
|
||||
}
|
||||
|
||||
CONNECTION_STATUS_PLANNED = False
|
||||
CONNECTION_STATUS_CONNECTED = True
|
||||
CONNECTION_STATUS_CHOICES = [
|
||||
@@ -211,7 +231,9 @@ class Region(MPTTModel):
|
||||
"""
|
||||
Sites can be grouped within geographic Regions.
|
||||
"""
|
||||
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
|
||||
parent = TreeForeignKey(
|
||||
'self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.CASCADE
|
||||
)
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
@@ -314,7 +336,7 @@ class RackGroup(models.Model):
|
||||
"""
|
||||
name = models.CharField(max_length=50)
|
||||
slug = models.SlugField()
|
||||
site = models.ForeignKey('Site', related_name='rack_groups')
|
||||
site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
@@ -573,6 +595,15 @@ class RackReservation(models.Model):
|
||||
)
|
||||
})
|
||||
|
||||
@property
|
||||
def unit_list(self):
|
||||
"""
|
||||
Express the assigned units as a string of summarized ranges. For example:
|
||||
[0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"
|
||||
"""
|
||||
group = (list(x) for _, x in groupby(sorted(self.units), lambda x, c=count(): next(c) - x))
|
||||
return ', '.join('-'.join(map(str, (g[0], g[-1])[:len(g)])) for g in group)
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
@@ -783,9 +814,9 @@ class InterfaceManager(models.Manager):
|
||||
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
|
||||
|
||||
To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
|
||||
slot, subslot, position, and channel:
|
||||
slot, subslot, position, channel, and virtual circuit:
|
||||
|
||||
{name}{slot}/{subslot}/{position}:{channel}
|
||||
{name}{slot}/{subslot}/{position}:{channel}.{vc}
|
||||
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
||||
be parsed as follows:
|
||||
@@ -795,21 +826,23 @@ class InterfaceManager(models.Manager):
|
||||
subslot = 0
|
||||
position = 1
|
||||
channel = None
|
||||
vc = 0
|
||||
|
||||
The chosen sorting method will determine which fields are ordered first in the query.
|
||||
"""
|
||||
queryset = self.get_queryset()
|
||||
sql_col = '{}.name'.format(queryset.model._meta.db_table)
|
||||
ordering = {
|
||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_name'),
|
||||
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel'),
|
||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_name'),
|
||||
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel', '_vc'),
|
||||
}[method]
|
||||
return queryset.extra(select={
|
||||
'_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_channel': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col),
|
||||
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
|
||||
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
|
||||
}).order_by(*ordering)
|
||||
|
||||
|
||||
@@ -919,19 +952,26 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
|
||||
name = NullableCharField(max_length=64, blank=True, null=True, unique=True)
|
||||
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
||||
asset_tag = NullableCharField(max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
|
||||
help_text='A unique tag used to identify this device')
|
||||
asset_tag = NullableCharField(
|
||||
max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
|
||||
help_text='A unique tag used to identify this device'
|
||||
)
|
||||
site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT)
|
||||
rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT)
|
||||
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
|
||||
verbose_name='Position (U)',
|
||||
help_text='The lowest-numbered unit occupied by the device')
|
||||
position = models.PositiveSmallIntegerField(
|
||||
blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)',
|
||||
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,
|
||||
blank=True, null=True, verbose_name='Primary IPv4')
|
||||
primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL,
|
||||
blank=True, null=True, verbose_name='Primary IPv6')
|
||||
status = models.PositiveSmallIntegerField(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, blank=True, null=True,
|
||||
verbose_name='Primary IPv4'
|
||||
)
|
||||
primary_ip6 = models.OneToOneField(
|
||||
'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True,
|
||||
verbose_name='Primary IPv6'
|
||||
)
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
images = GenericRelation(ImageAttachment)
|
||||
@@ -1051,6 +1091,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.platform.name if self.platform else None,
|
||||
self.serial,
|
||||
self.asset_tag,
|
||||
self.get_status_display(),
|
||||
self.site.name,
|
||||
self.rack.name if self.rack else None,
|
||||
self.position,
|
||||
@@ -1094,6 +1135,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
return Device.objects.filter(parent_bay__device=self.pk)
|
||||
|
||||
def get_status_class(self):
|
||||
return DEVICE_STATUS_CLASSES[self.status]
|
||||
|
||||
def get_rpc_client(self):
|
||||
"""
|
||||
Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.
|
||||
|
||||
@@ -6,7 +6,7 @@ from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, Region, Site,
|
||||
RackGroup, RackReservation, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
@@ -64,6 +64,12 @@ RACK_ROLE = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKRESERVATION_ACTIONS = """
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_ACTIONS = """
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@@ -86,12 +92,8 @@ DEVICE_ROLE = """
|
||||
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
|
||||
"""
|
||||
|
||||
STATUS_ICON = """
|
||||
{% if record.status %}
|
||||
<span class="glyphicon glyphicon-ok-sign text-success" title="Active" aria-hidden="true"></span>
|
||||
{% else %}
|
||||
<span class="glyphicon glyphicon-minus-sign text-danger" title="Offline" aria-hidden="true"></span>
|
||||
{% endif %}
|
||||
DEVICE_STATUS = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
|
||||
DEVICE_PRIMARY_IP = """
|
||||
@@ -247,6 +249,23 @@ class RackImportTable(BaseTable):
|
||||
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@@ -409,7 +428,7 @@ class PlatformTable(BaseTable):
|
||||
class DeviceTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
||||
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
@@ -429,7 +448,7 @@ class DeviceTable(BaseTable):
|
||||
|
||||
class DeviceSearchTable(SearchTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
||||
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
@@ -446,6 +465,7 @@ class DeviceSearchTable(SearchTable):
|
||||
|
||||
class DeviceImportTable(BaseTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
@@ -455,7 +475,7 @@ class DeviceImportTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('name', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
fields = ('name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
empty_text = False
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from .models import Device, Rack, Site
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'dcim'
|
||||
urlpatterns = [
|
||||
|
||||
# Regions
|
||||
@@ -39,11 +40,14 @@ urlpatterns = [
|
||||
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
|
||||
# Rack reservations
|
||||
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
||||
url(r'^rack-elevations/$', views.RackElevationListView.as_view(), name='rack_elevation_list'),
|
||||
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
|
||||
url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
|
||||
url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
|
||||
@@ -118,7 +122,6 @@ urlpatterns = [
|
||||
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
|
||||
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
||||
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'),
|
||||
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
|
||||
@@ -6,17 +6,20 @@ from operator import attrgetter
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
from django.db.models import Count
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
|
||||
from ipam.models import Prefix, IPAddress, Service, VLAN
|
||||
from ipam.models import Prefix, Service, VLAN
|
||||
from circuits.models import Circuit
|
||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
@@ -124,13 +127,13 @@ class ComponentCreateView(View):
|
||||
|
||||
class ComponentEditView(ObjectEditView):
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.device.get_absolute_url()
|
||||
|
||||
|
||||
class ComponentDeleteView(ObjectDeleteView):
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.device.get_absolute_url()
|
||||
|
||||
|
||||
@@ -149,7 +152,7 @@ class RegionEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Region
|
||||
form_class = forms.RegionForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:region_list')
|
||||
|
||||
|
||||
@@ -242,7 +245,7 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = RackGroup
|
||||
form_class = forms.RackGroupForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:rackgroup_list')
|
||||
|
||||
|
||||
@@ -268,7 +271,7 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = RackRole
|
||||
form_class = forms.RackRoleForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:rackrole_list')
|
||||
|
||||
|
||||
@@ -291,6 +294,46 @@ class RackListView(ObjectListView):
|
||||
template_name = 'dcim/rack_list.html'
|
||||
|
||||
|
||||
class RackElevationListView(View):
|
||||
"""
|
||||
Display a set of rack elevations side-by-side.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
|
||||
racks = Rack.objects.select_related(
|
||||
'site', 'group', 'tenant', 'role'
|
||||
).prefetch_related(
|
||||
'devices__device_type'
|
||||
)
|
||||
racks = filters.RackFilter(request.GET, racks).qs
|
||||
total_count = racks.count()
|
||||
|
||||
# Pagination
|
||||
paginator = EnhancedPaginator(racks, 25)
|
||||
page_number = request.GET.get('page', 1)
|
||||
try:
|
||||
page = paginator.page(page_number)
|
||||
except PageNotAnInteger:
|
||||
page = paginator.page(1)
|
||||
except EmptyPage:
|
||||
page = paginator.page(paginator.num_pages)
|
||||
|
||||
# Determine rack face
|
||||
if request.GET.get('face') == '1':
|
||||
face_id = 1
|
||||
else:
|
||||
face_id = 0
|
||||
|
||||
return render(request, 'dcim/rack_elevation_list.html', {
|
||||
'paginator': paginator,
|
||||
'page': page,
|
||||
'total_count': total_count,
|
||||
'face_id': face_id,
|
||||
'filter_form': forms.RackFilterForm(request.GET),
|
||||
})
|
||||
|
||||
|
||||
def rack(request, pk):
|
||||
|
||||
rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
|
||||
@@ -360,6 +403,14 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationListView(ObjectListView):
|
||||
queryset = RackReservation.objects.all()
|
||||
filter = filters.RackReservationFilter
|
||||
filter_form = forms.RackReservationFilterForm
|
||||
table = tables.RackReservationTable
|
||||
template_name = 'dcim/rackreservation_list.html'
|
||||
|
||||
|
||||
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_rackreservation'
|
||||
model = RackReservation
|
||||
@@ -371,7 +422,7 @@ class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
obj.user = request.user
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
@@ -379,10 +430,16 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
model = RackReservation
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
cls = RackReservation
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@@ -398,7 +455,7 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Manufacturer
|
||||
form_class = forms.ManufacturerForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:manufacturer_list')
|
||||
|
||||
|
||||
@@ -618,7 +675,7 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = DeviceRole
|
||||
form_class = forms.DeviceRoleForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:devicerole_list')
|
||||
|
||||
|
||||
@@ -643,7 +700,7 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Platform
|
||||
form_class = forms.PlatformForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:platform_list')
|
||||
|
||||
|
||||
@@ -686,19 +743,15 @@ def device(request, pk):
|
||||
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
|
||||
.filter(device=device, mgmt_only=False)\
|
||||
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
||||
'circuit_termination__circuit')
|
||||
'circuit_termination__circuit').prefetch_related('ip_addresses')
|
||||
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
|
||||
.filter(device=device, mgmt_only=True)\
|
||||
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
||||
'circuit_termination__circuit')
|
||||
'circuit_termination__circuit').prefetch_related('ip_addresses')
|
||||
device_bays = natsorted(
|
||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||
key=attrgetter('name')
|
||||
)
|
||||
|
||||
# Gather relevant device objects
|
||||
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\
|
||||
.order_by('address')
|
||||
services = Service.objects.filter(device=device)
|
||||
secrets = device.secrets.all()
|
||||
|
||||
@@ -729,7 +782,6 @@ def device(request, pk):
|
||||
'interfaces': interfaces,
|
||||
'mgmt_interfaces': mgmt_interfaces,
|
||||
'device_bays': device_bays,
|
||||
'ip_addresses': ip_addresses,
|
||||
'services': services,
|
||||
'secrets': secrets,
|
||||
'related_devices': related_devices,
|
||||
@@ -741,7 +793,6 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_device'
|
||||
model = Device
|
||||
form_class = forms.DeviceForm
|
||||
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
|
||||
template_name = 'dcim/device_edit.html'
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -842,12 +893,16 @@ def consoleport_connect(request, pk):
|
||||
form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
|
||||
if form.is_valid():
|
||||
consoleport = form.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
consoleport.device,
|
||||
consoleport.name,
|
||||
consoleport.cs_port.device,
|
||||
consoleport.cs_port.name,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
consoleport.device.get_absolute_url(),
|
||||
escape(consoleport.device),
|
||||
escape(consoleport.name),
|
||||
consoleport.cs_port.device.get_absolute_url(),
|
||||
escape(consoleport.cs_port.device),
|
||||
escape(consoleport.cs_port.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -871,17 +926,28 @@ def consoleport_disconnect(request, pk):
|
||||
consoleport = get_object_or_404(ConsolePort, pk=pk)
|
||||
|
||||
if not consoleport.cs_port:
|
||||
messages.warning(request, u"Cannot disconnect console port {}: It is not connected to anything."
|
||||
.format(consoleport))
|
||||
messages.warning(
|
||||
request, u"Cannot disconnect console port {}: It is not connected to anything.".format(consoleport)
|
||||
)
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
cs_port = consoleport.cs_port
|
||||
consoleport.cs_port = None
|
||||
consoleport.connection_status = None
|
||||
consoleport.save()
|
||||
messages.success(request, u"Console port {} has been disconnected.".format(consoleport))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
consoleport.device.get_absolute_url(),
|
||||
escape(consoleport.device),
|
||||
escape(consoleport.name),
|
||||
cs_port.device.get_absolute_url(),
|
||||
escape(cs_port.device),
|
||||
escape(cs_port.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -916,6 +982,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
form = forms.ConsoleConnectionImportForm
|
||||
table = tables.ConsoleConnectionTable
|
||||
template_name = 'dcim/console_connections_import.html'
|
||||
default_return_url = 'dcim:console_connections_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -943,12 +1010,16 @@ def consoleserverport_connect(request, pk):
|
||||
consoleport.cs_port = consoleserverport
|
||||
consoleport.connection_status = form.cleaned_data['connection_status']
|
||||
consoleport.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
consoleport.device,
|
||||
consoleport.name,
|
||||
consoleserverport.device,
|
||||
consoleserverport.name,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
consoleport.device.get_absolute_url(),
|
||||
escape(consoleport.device),
|
||||
escape(consoleport.name),
|
||||
consoleserverport.device.get_absolute_url(),
|
||||
escape(consoleserverport.device),
|
||||
escape(consoleserverport.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -972,8 +1043,9 @@ def consoleserverport_disconnect(request, pk):
|
||||
consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
|
||||
|
||||
if not hasattr(consoleserverport, 'connected_console'):
|
||||
messages.warning(request, u"Cannot disconnect console server port {}: Nothing is connected to it."
|
||||
.format(consoleserverport))
|
||||
messages.warning(
|
||||
request, u"Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport)
|
||||
)
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
@@ -983,7 +1055,16 @@ def consoleserverport_disconnect(request, pk):
|
||||
consoleport.cs_port = None
|
||||
consoleport.connection_status = None
|
||||
consoleport.save()
|
||||
messages.success(request, u"Console server port {} has been disconnected.".format(consoleserverport))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
consoleport.device.get_absolute_url(),
|
||||
escape(consoleport.device),
|
||||
escape(consoleport.name),
|
||||
consoleserverport.device.get_absolute_url(),
|
||||
escape(consoleserverport.device),
|
||||
escape(consoleserverport.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1035,12 +1116,16 @@ def powerport_connect(request, pk):
|
||||
form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
|
||||
if form.is_valid():
|
||||
powerport = form.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
powerport.device,
|
||||
powerport.name,
|
||||
powerport.power_outlet.device,
|
||||
powerport.power_outlet.name,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
powerport.device.get_absolute_url(),
|
||||
escape(powerport.device),
|
||||
escape(powerport.name),
|
||||
powerport.power_outlet.device.get_absolute_url(),
|
||||
escape(powerport.power_outlet.device),
|
||||
escape(powerport.power_outlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1064,17 +1149,28 @@ def powerport_disconnect(request, pk):
|
||||
powerport = get_object_or_404(PowerPort, pk=pk)
|
||||
|
||||
if not powerport.power_outlet:
|
||||
messages.warning(request, u"Cannot disconnect power port {}: It is not connected to an outlet."
|
||||
.format(powerport))
|
||||
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)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
power_outlet = powerport.power_outlet
|
||||
powerport.power_outlet = None
|
||||
powerport.connection_status = None
|
||||
powerport.save()
|
||||
messages.success(request, u"Power port {} has been disconnected.".format(powerport))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
powerport.device.get_absolute_url(),
|
||||
escape(powerport.device),
|
||||
escape(powerport.name),
|
||||
power_outlet.device.get_absolute_url(),
|
||||
escape(power_outlet.device),
|
||||
escape(power_outlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1109,6 +1205,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
form = forms.PowerConnectionImportForm
|
||||
table = tables.PowerConnectionTable
|
||||
template_name = 'dcim/power_connections_import.html'
|
||||
default_return_url = 'dcim:power_connections_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1136,12 +1233,16 @@ def poweroutlet_connect(request, pk):
|
||||
powerport.power_outlet = poweroutlet
|
||||
powerport.connection_status = form.cleaned_data['connection_status']
|
||||
powerport.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
powerport.device,
|
||||
powerport.name,
|
||||
poweroutlet.device,
|
||||
poweroutlet.name,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
powerport.device.get_absolute_url(),
|
||||
escape(powerport.device),
|
||||
escape(powerport.name),
|
||||
poweroutlet.device.get_absolute_url(),
|
||||
escape(poweroutlet.device),
|
||||
escape(poweroutlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1165,7 +1266,9 @@ def poweroutlet_disconnect(request, pk):
|
||||
poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
|
||||
|
||||
if not hasattr(poweroutlet, 'connected_port'):
|
||||
messages.warning(request, u"Cannot disconnect power outlet {}: 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':
|
||||
@@ -1175,7 +1278,16 @@ def poweroutlet_disconnect(request, pk):
|
||||
powerport.power_outlet = None
|
||||
powerport.connection_status = None
|
||||
powerport.save()
|
||||
messages.success(request, u"Power outlet {} has been disconnected.".format(poweroutlet))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
powerport.device.get_absolute_url(),
|
||||
escape(powerport.device),
|
||||
escape(powerport.name),
|
||||
poweroutlet.device.get_absolute_url(),
|
||||
escape(poweroutlet.device),
|
||||
escape(poweroutlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1441,13 +1553,19 @@ def interfaceconnection_add(request, pk):
|
||||
if request.method == 'POST':
|
||||
form = forms.InterfaceConnectionForm(device, request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
interfaceconnection = form.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
interfaceconnection.interface_a.device,
|
||||
interfaceconnection.interface_a,
|
||||
interfaceconnection.interface_b.device,
|
||||
interfaceconnection.interface_b,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
interfaceconnection.interface_a.device.get_absolute_url(),
|
||||
escape(interfaceconnection.interface_a.device),
|
||||
escape(interfaceconnection.interface_a.name),
|
||||
interfaceconnection.interface_b.device.get_absolute_url(),
|
||||
escape(interfaceconnection.interface_b.device),
|
||||
escape(interfaceconnection.interface_b.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
|
||||
device_b = interfaceconnection.interface_b.device
|
||||
@@ -1485,12 +1603,16 @@ def interfaceconnection_delete(request, pk):
|
||||
form = forms.InterfaceConnectionDeletionForm(request.POST)
|
||||
if form.is_valid():
|
||||
interfaceconnection.delete()
|
||||
messages.success(request, u"Deleted the connection between {} {} and {} {}.".format(
|
||||
interfaceconnection.interface_a.device,
|
||||
interfaceconnection.interface_a,
|
||||
interfaceconnection.interface_b.device,
|
||||
interfaceconnection.interface_b,
|
||||
))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
interfaceconnection.interface_a.device.get_absolute_url(),
|
||||
escape(interfaceconnection.interface_a.device),
|
||||
escape(interfaceconnection.interface_a.name),
|
||||
interfaceconnection.interface_b.device.get_absolute_url(),
|
||||
escape(interfaceconnection.interface_b.device),
|
||||
escape(interfaceconnection.interface_b.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
|
||||
if form.cleaned_data['device']:
|
||||
return redirect('dcim:device', pk=form.cleaned_data['device'].pk)
|
||||
else:
|
||||
@@ -1520,6 +1642,7 @@ class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView
|
||||
form = forms.InterfaceConnectionImportForm
|
||||
table = tables.InterfaceConnectionTable
|
||||
template_name = 'dcim/interface_connections_import.html'
|
||||
default_return_url = 'dcim:interface_connections_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1553,47 +1676,6 @@ class InterfaceConnectionsListView(ObjectListView):
|
||||
template_name = 'dcim/interface_connections_list.html'
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
@permission_required(['dcim.change_device', 'ipam.add_ipaddress'])
|
||||
def ipaddress_assign(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.IPAddressForm(device, request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
ipaddress = form.save(commit=False)
|
||||
ipaddress.interface = form.cleaned_data['interface']
|
||||
ipaddress.save()
|
||||
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:
|
||||
device.primary_ip4 = ipaddress
|
||||
elif ipaddress.family == 6:
|
||||
device.primary_ip6 = ipaddress
|
||||
device.save()
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:ipaddress_assign', pk=device.pk)
|
||||
else:
|
||||
return redirect('dcim:device', pk=device.pk)
|
||||
|
||||
else:
|
||||
form = forms.IPAddressForm(device)
|
||||
|
||||
return render(request, 'dcim/ipaddress_assign.html', {
|
||||
'device': device,
|
||||
'form': form,
|
||||
'return_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue
|
||||
|
||||
@@ -14,12 +16,40 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
def to_representation(self, obj):
|
||||
return obj
|
||||
|
||||
def to_internal_value(self, data):
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
|
||||
custom_fields = {field.name: field for field in CustomField.objects.filter(obj_type=content_type)}
|
||||
|
||||
for field_name, value in data.items():
|
||||
|
||||
# Validate custom field name
|
||||
if field_name not in custom_fields:
|
||||
raise ValidationError(u"Invalid custom field for {} objects: {}".format(content_type, field_name))
|
||||
|
||||
# Validate selected choice
|
||||
cf = custom_fields[field_name]
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
valid_choices = [c.pk for c in cf.choices.all()]
|
||||
if value not in valid_choices:
|
||||
raise ValidationError(u"Invalid choice ({}) for field {}".format(value, field_name))
|
||||
|
||||
# Check for missing required fields
|
||||
missing_fields = []
|
||||
for field_name, field in custom_fields.items():
|
||||
if field.required and field_name not in data:
|
||||
missing_fields.append(field_name)
|
||||
if missing_fields:
|
||||
raise ValidationError(u"Missing required fields: {}".format(u", ".join(missing_fields)))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class CustomFieldModelSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Extends ModelSerializer to render any CustomFields and their values associated with an object.
|
||||
"""
|
||||
custom_fields = CustomFieldsSerializer()
|
||||
custom_fields = CustomFieldsSerializer(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -34,16 +64,59 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
super(CustomFieldModelSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
# Retrieve the set of CustomFields which apply to this type of object
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
fields = CustomField.objects.filter(obj_type=content_type)
|
||||
if self.instance is not None:
|
||||
|
||||
# Populate CustomFieldValues for each instance from database
|
||||
try:
|
||||
for obj in self.instance:
|
||||
_populate_custom_fields(obj, fields)
|
||||
except TypeError:
|
||||
_populate_custom_fields(self.instance, fields)
|
||||
# Retrieve the set of CustomFields which apply to this type of object
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
fields = CustomField.objects.filter(obj_type=content_type)
|
||||
|
||||
# Populate CustomFieldValues for each instance from database
|
||||
try:
|
||||
for obj in self.instance:
|
||||
_populate_custom_fields(obj, fields)
|
||||
except TypeError:
|
||||
_populate_custom_fields(self.instance, fields)
|
||||
|
||||
def _save_custom_fields(self, instance, custom_fields):
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
for field_name, value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=field_name)
|
||||
CustomFieldValue.objects.update_or_create(
|
||||
field=custom_field,
|
||||
obj_type=content_type,
|
||||
obj_id=instance.pk,
|
||||
defaults={'serialized_value': value},
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
custom_fields = validated_data.pop('custom_fields', None)
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
instance = super(CustomFieldModelSerializer, self).create(validated_data)
|
||||
|
||||
# Save custom fields
|
||||
if custom_fields is not None:
|
||||
self._save_custom_fields(instance, custom_fields)
|
||||
instance.custom_fields = custom_fields
|
||||
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
custom_fields = validated_data.pop('custom_fields', None)
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
instance = super(CustomFieldModelSerializer, self).update(instance, validated_data)
|
||||
|
||||
# Save custom fields
|
||||
if custom_fields is not None:
|
||||
self._save_custom_fields(instance, custom_fields)
|
||||
instance.custom_fields = custom_fields
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class CustomFieldChoiceSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -29,4 +29,5 @@ router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
||||
# Recent activity
|
||||
router.register(r'recent-activity', views.RecentActivityViewSet)
|
||||
|
||||
app_name = 'extras-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
from dcim.models import Device, InventoryItem, Site
|
||||
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -39,7 +39,7 @@ class Command(BaseCommand):
|
||||
self.password = getpass("Password: ")
|
||||
|
||||
# Attempt to inventory only active devices
|
||||
device_list = Device.objects.filter(status=True)
|
||||
device_list = Device.objects.filter(status=STATUS_ACTIVE)
|
||||
|
||||
# --site: Include only devices belonging to specified site(s)
|
||||
if options['site']:
|
||||
@@ -72,7 +72,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Skip inactive devices
|
||||
if not device.status:
|
||||
self.stdout.write("Skipped (inactive)")
|
||||
self.stdout.write("Skipped (not active)")
|
||||
continue
|
||||
|
||||
# Skip devices without primary_ip set
|
||||
|
||||
@@ -156,16 +156,13 @@ class CustomField(models.Model):
|
||||
# Read date as YYYY-MM-DD
|
||||
return date(*[int(n) for n in serialized_value.split('-')])
|
||||
if self.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
return self.choices.get(pk=int(serialized_value))
|
||||
except CustomFieldChoice.DoesNotExist:
|
||||
return None
|
||||
return self.choices.get(pk=int(serialized_value))
|
||||
return serialized_value
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CustomFieldValue(models.Model):
|
||||
field = models.ForeignKey('CustomField', related_name='values')
|
||||
field = models.ForeignKey('CustomField', related_name='values', on_delete=models.CASCADE)
|
||||
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
|
||||
obj_id = models.PositiveIntegerField()
|
||||
obj = GenericForeignKey('obj_type', 'obj_id')
|
||||
@@ -254,7 +251,9 @@ class Graph(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ExportTemplate(models.Model):
|
||||
content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
|
||||
content_type = models.ForeignKey(
|
||||
ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}, on_delete=models.CASCADE
|
||||
)
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.CharField(max_length=200, blank=True)
|
||||
template_code = models.TextField()
|
||||
@@ -294,7 +293,7 @@ class ExportTemplate(models.Model):
|
||||
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)
|
||||
site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True, on_delete=models.CASCADE)
|
||||
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. "
|
||||
@@ -316,6 +315,7 @@ class TopologyMap(models.Model):
|
||||
|
||||
def render(self, img_format='png'):
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import Device, InterfaceConnection
|
||||
|
||||
# Construct the graph
|
||||
@@ -334,9 +334,10 @@ class TopologyMap(models.Model):
|
||||
# Add each device to the graph
|
||||
devices = []
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
devices += Device.objects.filter(name__regex=query)
|
||||
devices += Device.objects.filter(name__regex=query).select_related('device_role')
|
||||
for d in devices:
|
||||
subgraph.node(d.name)
|
||||
fillcolor = '#{}'.format(d.device_role.color)
|
||||
subgraph.node(d.name, style='filled', fillcolor=fillcolor)
|
||||
|
||||
# Add an invisible connection to each successive device in a set to enforce horizontal order
|
||||
for j in range(0, len(devices) - 1):
|
||||
@@ -350,7 +351,7 @@ class TopologyMap(models.Model):
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
device_superset = device_superset | Q(name__regex=query)
|
||||
|
||||
# Add all connections to the graph
|
||||
# Add all interface connections to the graph
|
||||
devices = Device.objects.filter(*(device_superset,))
|
||||
connections = InterfaceConnection.objects.filter(
|
||||
interface_a__device__in=devices, interface_b__device__in=devices
|
||||
@@ -358,6 +359,12 @@ class TopologyMap(models.Model):
|
||||
for c in connections:
|
||||
graph.edge(c.interface_a.device.name, c.interface_b.device.name)
|
||||
|
||||
# Add all circuits to the graph
|
||||
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
|
||||
peer_termination = termination.get_peer_termination()
|
||||
if peer_termination is not None and peer_termination.interface.device in devices:
|
||||
graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
|
||||
|
||||
return graph.pipe(format=img_format)
|
||||
|
||||
|
||||
@@ -384,7 +391,7 @@ class ImageAttachment(models.Model):
|
||||
"""
|
||||
An uploaded image which is associated with an object.
|
||||
"""
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
parent = GenericForeignKey('content_type', 'object_id')
|
||||
image = models.ImageField(upload_to=image_upload, height_field='image_height', width_field='image_width')
|
||||
@@ -415,6 +422,16 @@ class ImageAttachment(models.Model):
|
||||
# before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.)
|
||||
self.image.name = _name
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""
|
||||
Wrapper around `image.size` to suppress an OSError in case the file is inaccessible.
|
||||
"""
|
||||
try:
|
||||
return self.image.size
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
from datetime import date
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.models import Site
|
||||
|
||||
@@ -9,9 +14,11 @@ from extras.models import (
|
||||
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
|
||||
CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
)
|
||||
from users.models import Token
|
||||
from utilities.tests import HttpStatusMixin
|
||||
|
||||
|
||||
class CustomFieldTestCase(TestCase):
|
||||
class CustomFieldTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -95,3 +102,209 @@ class CustomFieldTestCase(TestCase):
|
||||
|
||||
# Delete the custom field
|
||||
cf.delete()
|
||||
|
||||
|
||||
class CustomFieldAPITest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Site)
|
||||
|
||||
# Text custom field
|
||||
self.cf_text = CustomField(type=CF_TYPE_TEXT, name='magic_word')
|
||||
self.cf_text.save()
|
||||
self.cf_text.obj_type = [content_type]
|
||||
self.cf_text.save()
|
||||
|
||||
# Integer custom field
|
||||
self.cf_integer = CustomField(type=CF_TYPE_INTEGER, name='magic_number')
|
||||
self.cf_integer.save()
|
||||
self.cf_integer.obj_type = [content_type]
|
||||
self.cf_integer.save()
|
||||
|
||||
# Boolean custom field
|
||||
self.cf_boolean = CustomField(type=CF_TYPE_BOOLEAN, name='is_magic')
|
||||
self.cf_boolean.save()
|
||||
self.cf_boolean.obj_type = [content_type]
|
||||
self.cf_boolean.save()
|
||||
|
||||
# Date custom field
|
||||
self.cf_date = CustomField(type=CF_TYPE_DATE, name='magic_date')
|
||||
self.cf_date.save()
|
||||
self.cf_date.obj_type = [content_type]
|
||||
self.cf_date.save()
|
||||
|
||||
# URL custom field
|
||||
self.cf_url = CustomField(type=CF_TYPE_URL, name='magic_url')
|
||||
self.cf_url.save()
|
||||
self.cf_url.obj_type = [content_type]
|
||||
self.cf_url.save()
|
||||
|
||||
# Select custom field
|
||||
self.cf_select = CustomField(type=CF_TYPE_SELECT, name='magic_choice')
|
||||
self.cf_select.save()
|
||||
self.cf_select.obj_type = [content_type]
|
||||
self.cf_select.save()
|
||||
self.cf_select_choice1 = CustomFieldChoice(field=self.cf_select, value='Foo')
|
||||
self.cf_select_choice1.save()
|
||||
self.cf_select_choice2 = CustomFieldChoice(field=self.cf_select, value='Bar')
|
||||
self.cf_select_choice2.save()
|
||||
self.cf_select_choice3 = CustomFieldChoice(field=self.cf_select, value='Baz')
|
||||
self.cf_select_choice3.save()
|
||||
|
||||
self.site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
|
||||
def test_get_obj_without_custom_fields(self):
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.site.name)
|
||||
self.assertEqual(response.data['custom_fields'], {
|
||||
'magic_word': None,
|
||||
'magic_number': None,
|
||||
'is_magic': None,
|
||||
'magic_date': None,
|
||||
'magic_url': None,
|
||||
'magic_choice': None,
|
||||
})
|
||||
|
||||
def test_get_obj_with_custom_fields(self):
|
||||
|
||||
CUSTOM_FIELD_VALUES = [
|
||||
(self.cf_text, 'Test string'),
|
||||
(self.cf_integer, 1234),
|
||||
(self.cf_boolean, True),
|
||||
(self.cf_date, date(2016, 6, 23)),
|
||||
(self.cf_url, 'http://example.com/'),
|
||||
(self.cf_select, self.cf_select_choice1.pk),
|
||||
]
|
||||
for field, value in CUSTOM_FIELD_VALUES:
|
||||
cfv = CustomFieldValue(field=field, obj=self.site)
|
||||
cfv.value = value
|
||||
cfv.save()
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.site.name)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_word'), CUSTOM_FIELD_VALUES[0][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_number'), CUSTOM_FIELD_VALUES[1][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('is_magic'), CUSTOM_FIELD_VALUES[2][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_date'), CUSTOM_FIELD_VALUES[3][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_url'), CUSTOM_FIELD_VALUES[4][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_choice'), {
|
||||
'value': self.cf_select_choice1.pk, 'label': 'Foo'
|
||||
})
|
||||
|
||||
def test_set_custom_field_text(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_word': 'Foo bar baz',
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_word'), data['custom_fields']['magic_word'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_text)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['magic_word'])
|
||||
|
||||
def test_set_custom_field_integer(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_number': 42,
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_number'), data['custom_fields']['magic_number'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_integer)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['magic_number'])
|
||||
|
||||
def test_set_custom_field_boolean(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'is_magic': 0,
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('is_magic'), data['custom_fields']['is_magic'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_boolean)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['is_magic'])
|
||||
|
||||
def test_set_custom_field_date(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_date': '2017-04-25',
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_date'), data['custom_fields']['magic_date'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_date)
|
||||
self.assertEqual(cfv.value.isoformat(), data['custom_fields']['magic_date'])
|
||||
|
||||
def test_set_custom_field_url(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_url': 'http://example.com/2/',
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_url'), data['custom_fields']['magic_url'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_url)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['magic_url'])
|
||||
|
||||
def test_set_custom_field_select(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_choice': self.cf_select_choice2.pk,
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_choice'), data['custom_fields']['magic_choice'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_select)
|
||||
self.assertEqual(cfv.value.pk, data['custom_fields']['magic_choice'])
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.conf.urls import url
|
||||
from extras import views
|
||||
|
||||
|
||||
app_name = 'extras'
|
||||
urlpatterns = [
|
||||
|
||||
# Image attachments
|
||||
|
||||
@@ -18,7 +18,7 @@ class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
imageattachment.parent = get_object_or_404(model, pk=kwargs['object_id'])
|
||||
return imageattachment
|
||||
|
||||
def get_return_url(self, imageattachment):
|
||||
def get_return_url(self, request, imageattachment):
|
||||
return imageattachment.parent.get_absolute_url()
|
||||
|
||||
|
||||
@@ -26,5 +26,5 @@ class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_imageattachment'
|
||||
model = ImageAttachment
|
||||
|
||||
def get_return_url(self, imageattachment):
|
||||
return imageattachment.obj.get_absolute_url()
|
||||
def get_return_url(self, request, imageattachment):
|
||||
return imageattachment.parent.get_absolute_url()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This script will generate a random 50-character string suitable for use as a SECRET_KEY.
|
||||
import os
|
||||
import random
|
||||
|
||||
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
|
||||
random.seed = (os.urandom(2048))
|
||||
print(''.join(random.choice(charset) for c in range(50)))
|
||||
secure_random = random.SystemRandom()
|
||||
print(''.join(secure_random.sample(charset, 50)))
|
||||
|
||||
@@ -31,11 +31,11 @@ class NestedVRFSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'name', 'rd']
|
||||
|
||||
|
||||
class WritableVRFSerializer(serializers.ModelSerializer):
|
||||
class WritableVRFSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields']
|
||||
|
||||
|
||||
#
|
||||
@@ -96,11 +96,11 @@ class NestedAggregateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'family', 'prefix']
|
||||
|
||||
|
||||
class WritableAggregateSerializer(serializers.ModelSerializer):
|
||||
class WritableAggregateSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['id', 'prefix', 'rir', 'date_added', 'description']
|
||||
fields = ['id', 'prefix', 'rir', 'date_added', 'description', 'custom_fields']
|
||||
|
||||
|
||||
#
|
||||
@@ -169,11 +169,11 @@ class NestedVLANSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
class WritableVLANSerializer(serializers.ModelSerializer):
|
||||
class WritableVLANSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'custom_fields']
|
||||
validators = []
|
||||
|
||||
def validate(self, data):
|
||||
@@ -216,11 +216,14 @@ class NestedPrefixSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'family', 'prefix']
|
||||
|
||||
|
||||
class WritablePrefixSerializer(serializers.ModelSerializer):
|
||||
class WritablePrefixSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['id', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description']
|
||||
fields = [
|
||||
'id', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
|
||||
'custom_fields',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
@@ -252,11 +255,11 @@ IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer()
|
||||
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer()
|
||||
|
||||
|
||||
class WritableIPAddressSerializer(serializers.ModelSerializer):
|
||||
class WritableIPAddressSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside']
|
||||
fields = ['id', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', 'custom_fields']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -37,4 +37,5 @@ router.register(r'vlans', views.VLANViewSet)
|
||||
# Services
|
||||
router.register(r'services', views.ServiceViewSet)
|
||||
|
||||
app_name = 'ipam-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -9,7 +9,10 @@ from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from .models import (
|
||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
||||
VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
||||
)
|
||||
|
||||
|
||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
@@ -153,10 +156,13 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=PREFIX_STATUS_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['family', 'status']
|
||||
fields = ['family']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -237,10 +243,13 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
queryset=Interface.objects.all(),
|
||||
label='Interface (ID)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=IPADDRESS_STATUS_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['family', 'status']
|
||||
fields = ['family']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -337,10 +346,13 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=VLAN_STATUS_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['name', 'vid', 'status']
|
||||
fields = ['name', 'vid']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -5,8 +5,8 @@ from dcim.models import Site, Rack, Device, Interface
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch,
|
||||
SlugField, add_blank_choice,
|
||||
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, CSVDataField, ExpandableIPAddressField,
|
||||
FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
@@ -61,6 +61,9 @@ class VRFImportForm(BootstrapMixin, BulkImportForm):
|
||||
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
enforce_unique = forms.NullBooleanField(
|
||||
required=False, widget=BulkEditNullBooleanSelect, label='Enforce unique space'
|
||||
)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -210,28 +213,32 @@ class PrefixFromCSVForm(forms.ModelForm):
|
||||
site = self.cleaned_data.get('site')
|
||||
vlan_group_name = self.cleaned_data.get('vlan_group_name')
|
||||
vlan_vid = self.cleaned_data.get('vlan_vid')
|
||||
|
||||
# Validate VLAN
|
||||
vlan_group = None
|
||||
vlan = None
|
||||
|
||||
# Validate VLAN group
|
||||
if vlan_group_name:
|
||||
try:
|
||||
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
|
||||
except VLANGroup.DoesNotExist:
|
||||
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
|
||||
if vlan_vid and vlan_group:
|
||||
if site:
|
||||
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
|
||||
else:
|
||||
self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name))
|
||||
|
||||
# Validate VLAN
|
||||
if vlan_vid:
|
||||
try:
|
||||
self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid)
|
||||
self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid)
|
||||
except VLAN.DoesNotExist:
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid))
|
||||
elif vlan_vid and site:
|
||||
try:
|
||||
self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid)
|
||||
except VLAN.DoesNotExist:
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
|
||||
if site:
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
|
||||
elif vlan_group:
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name))
|
||||
elif not vlan_group_name:
|
||||
self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid))
|
||||
except VLAN.MultipleObjectsReturned:
|
||||
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
|
||||
elif vlan_vid:
|
||||
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -252,6 +259,7 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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)
|
||||
is_pool = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a pool')
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -302,21 +310,47 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
nat_site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
||||
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')
|
||||
class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
|
||||
interface_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
|
||||
attrs={'filter-for': 'interface_rack'}
|
||||
)
|
||||
)
|
||||
interface_rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(), required=False, label='Rack', widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{interface_site}}', display_field='display_name',
|
||||
attrs={'filter-for': 'interface_device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
interface_device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(), required=False, label='Device', widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}',
|
||||
display_field='display_name', attrs={'filter-for': 'interface'}
|
||||
)
|
||||
)
|
||||
nat_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), required=False, label='Site', 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'
|
||||
)
|
||||
)
|
||||
primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device')
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'nat_inside', 'description']
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside']
|
||||
widgets = {
|
||||
'interface': APISelect(api_url='/api/dcim/interfaces/?device_id={{interface_device}}'),
|
||||
'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
|
||||
}
|
||||
|
||||
@@ -325,8 +359,46 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
if self.instance.nat_inside:
|
||||
# If an interface has been assigned, initialize site, rack, and device
|
||||
if self.instance.interface:
|
||||
self.initial['interface_site'] = self.instance.interface.device.site
|
||||
self.initial['interface_rack'] = self.instance.interface.device.rack
|
||||
self.initial['interface_device'] = self.instance.interface.device
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound and self.data.get('interface_site'):
|
||||
self.fields['interface_rack'].queryset = Rack.objects.filter(site__pk=self.data['interface_site'])
|
||||
elif self.initial.get('interface_site'):
|
||||
self.fields['interface_rack'].queryset = Rack.objects.filter(site=self.initial['interface_site'])
|
||||
else:
|
||||
self.fields['interface_rack'].choices = []
|
||||
|
||||
# Limit device choices
|
||||
if self.is_bound and self.data.get('interface_rack'):
|
||||
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.data['interface_rack'])
|
||||
elif self.initial.get('interface_rack'):
|
||||
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.initial['interface_rack'])
|
||||
else:
|
||||
self.fields['interface_device'].choices = []
|
||||
|
||||
# Limit interface choices
|
||||
if self.is_bound and self.data.get('interface_device'):
|
||||
self.fields['interface'].queryset = Interface.objects.filter(device=self.data['interface_device'])
|
||||
elif self.initial.get('interface_device'):
|
||||
self.fields['interface'].queryset = Interface.objects.filter(device=self.initial['interface_device'])
|
||||
else:
|
||||
self.fields['interface'].choices = []
|
||||
|
||||
# Initialize primary_for_device if IP address is already assigned
|
||||
if self.instance.interface is not None:
|
||||
device = self.instance.interface.device
|
||||
if (
|
||||
self.instance.address.version == 4 and device.primary_ip4 == self.instance or
|
||||
self.instance.address.version == 6 and device.primary_ip6 == self.instance
|
||||
):
|
||||
self.initial['primary_for_device'] = True
|
||||
|
||||
if self.instance.nat_inside:
|
||||
nat_inside = self.instance.nat_inside
|
||||
# If the IP is assigned to an interface, populate site/device fields accordingly
|
||||
if self.instance.nat_inside.interface:
|
||||
@@ -340,9 +412,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
)
|
||||
else:
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk)
|
||||
|
||||
else:
|
||||
|
||||
# Initialize nat_device choices if nat_site is set
|
||||
if self.is_bound and self.data.get('nat_site'):
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(site__pk=self.data['nat_site'])
|
||||
@@ -350,7 +420,6 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(site=self.initial['nat_site'])
|
||||
else:
|
||||
self.fields['nat_device'].choices = []
|
||||
|
||||
# Initialize nat_inside choices if nat_device is set
|
||||
if self.is_bound and self.data.get('nat_device'):
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
||||
@@ -361,72 +430,53 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
else:
|
||||
self.fields['nat_inside'].choices = []
|
||||
|
||||
def clean(self):
|
||||
super(IPAddressForm, self).clean()
|
||||
|
||||
class IPAddressBulkAddForm(BootstrapMixin, forms.Form):
|
||||
address = ExpandableIPAddressField()
|
||||
# Primary IP assignment is only available if an interface has been assigned.
|
||||
if self.cleaned_data.get('primary_for_device') and not self.cleaned_data.get('interface'):
|
||||
self.add_error(
|
||||
'primary_for_device', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
|
||||
|
||||
# Assign this IPAddress as the primary for the associated Device.
|
||||
if self.cleaned_data['primary_for_device']:
|
||||
device = self.cleaned_data['interface'].device
|
||||
if ipaddress.address.version == 4:
|
||||
device.primary_ip4 = ipaddress
|
||||
else:
|
||||
device.primary_ip6 = ipaddress
|
||||
device.save()
|
||||
|
||||
# Clear assignment as primary for device if set.
|
||||
else:
|
||||
try:
|
||||
if ipaddress.address.version == 4:
|
||||
device = ipaddress.primary_ip4_for
|
||||
device.primary_ip4 = None
|
||||
else:
|
||||
device = ipaddress.primary_ip6_for
|
||||
device.primary_ip6 = None
|
||||
device.save()
|
||||
except Device.DoesNotExist:
|
||||
pass
|
||||
|
||||
return ipaddress
|
||||
|
||||
|
||||
class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm):
|
||||
address_pattern = ExpandableIPAddressField(label='Address Pattern')
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=IPADDRESS_STATUS_CHOICES)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
pattern_map = ('address_pattern', 'address')
|
||||
|
||||
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', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&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/interfaces/?device_id={{device}}'
|
||||
)
|
||||
)
|
||||
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 Meta:
|
||||
model = IPAddress
|
||||
fields = ['address_pattern', 'vrf', 'tenant', 'status', 'description']
|
||||
|
||||
|
||||
class IPAddressFromCSVForm(forms.ModelForm):
|
||||
@@ -586,27 +636,51 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
|
||||
class VLANFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
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,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'}
|
||||
)
|
||||
group_name = forms.CharField(required=False)
|
||||
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 VLAN_STATUS_CHOICES])
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid role.'})
|
||||
role = forms.ModelChoiceField(
|
||||
queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid role.'}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
|
||||
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(VLANFromCSVForm, self).clean()
|
||||
|
||||
# Validate VLANGroup
|
||||
group_name = self.cleaned_data.get('group_name')
|
||||
if group_name:
|
||||
try:
|
||||
vlan_group = VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
|
||||
except VLANGroup.DoesNotExist:
|
||||
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
m = super(VLANFromCSVForm, self).save(commit=False)
|
||||
|
||||
vlan = super(VLANFromCSVForm, self).save(commit=False)
|
||||
|
||||
# Assign VLANGroup by site and name
|
||||
if self.cleaned_data['group_name']:
|
||||
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
|
||||
|
||||
# Assign VLAN status by name
|
||||
m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
vlan.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
|
||||
if kwargs.get('commit'):
|
||||
m.save()
|
||||
return m
|
||||
vlan.save()
|
||||
return vlan
|
||||
|
||||
|
||||
class VLANImportForm(BootstrapMixin, BulkImportForm):
|
||||
|
||||
@@ -3,10 +3,10 @@ from netaddr import IPNetwork, cidr_merge
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from dcim.models import Interface
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'ipam'
|
||||
urlpatterns = [
|
||||
|
||||
# VRFs
|
||||
@@ -57,8 +58,6 @@ 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
|
||||
|
||||
@@ -5,9 +5,9 @@ from django.conf import settings
|
||||
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, redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.forms import ConfirmationForm
|
||||
@@ -244,7 +244,7 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = RIR
|
||||
form_class = forms.RIRForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('ipam:rir_list')
|
||||
|
||||
|
||||
@@ -370,7 +370,7 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Role
|
||||
form_class = forms.RoleForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('ipam:role_list')
|
||||
|
||||
|
||||
@@ -464,7 +464,6 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Prefix
|
||||
form_class = forms.PrefixForm
|
||||
template_name = 'ipam/prefix_edit.html'
|
||||
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
@@ -572,80 +571,10 @@ 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:
|
||||
assert False, form.errors
|
||||
|
||||
else:
|
||||
form = forms.IPAddressAssignForm()
|
||||
|
||||
return render(request, 'ipam/ipaddress_assign.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'form': form,
|
||||
'return_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,
|
||||
'return_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'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
@@ -659,7 +588,7 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
form = forms.IPAddressBulkAddForm
|
||||
model = IPAddress
|
||||
model_form = forms.IPAddressForm
|
||||
template_name = 'ipam/ipaddress_bulk_add.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
@@ -718,7 +647,7 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = VLANGroup
|
||||
form_class = forms.VLANGroupForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('ipam:vlangroup_list')
|
||||
|
||||
|
||||
@@ -807,7 +736,7 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.device.get_absolute_url()
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.0-beta2'
|
||||
VERSION = '2.0.0'
|
||||
|
||||
# Import local configuration
|
||||
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from rest_framework_swagger.views import get_swagger_view
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
@@ -8,6 +10,7 @@ from users.views import login, logout
|
||||
|
||||
|
||||
handler500 = handle_500
|
||||
swagger_view = get_swagger_view(title='NetBox API')
|
||||
|
||||
_patterns = [
|
||||
|
||||
@@ -20,23 +23,23 @@ _patterns = [
|
||||
url(r'^logout/$', logout, name='logout'),
|
||||
|
||||
# Apps
|
||||
url(r'^circuits/', include('circuits.urls', namespace='circuits')),
|
||||
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
|
||||
url(r'^extras/', include('extras.urls', namespace='extras')),
|
||||
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
|
||||
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
|
||||
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
|
||||
url(r'^user/', include('users.urls', namespace='user')),
|
||||
url(r'^circuits/', include('circuits.urls')),
|
||||
url(r'^dcim/', include('dcim.urls')),
|
||||
url(r'^extras/', include('extras.urls')),
|
||||
url(r'^ipam/', include('ipam.urls')),
|
||||
url(r'^secrets/', include('secrets.urls')),
|
||||
url(r'^tenancy/', include('tenancy.urls')),
|
||||
url(r'^user/', include('users.urls')),
|
||||
|
||||
# API
|
||||
url(r'^api/$', APIRootView.as_view(), name='api-root'),
|
||||
url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')),
|
||||
url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
|
||||
url(r'^api/extras/', include('extras.api.urls', namespace='extras-api')),
|
||||
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
|
||||
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
|
||||
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
|
||||
url(r'^api/docs/', include('rest_framework_swagger.urls')),
|
||||
url(r'^api/circuits/', include('circuits.api.urls')),
|
||||
url(r'^api/dcim/', include('dcim.api.urls')),
|
||||
url(r'^api/extras/', include('extras.api.urls')),
|
||||
url(r'^api/ipam/', include('ipam.api.urls')),
|
||||
url(r'^api/secrets/', include('secrets.api.urls')),
|
||||
url(r'^api/tenancy/', include('tenancy.api.urls')),
|
||||
url(r'^api/docs/', swagger_view, name='api_docs'),
|
||||
|
||||
# Serving static media in Django to pipe it through LoginRequiredMiddleware
|
||||
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||
@@ -45,7 +48,7 @@ _patterns = [
|
||||
url(r'^500/$', trigger_500),
|
||||
|
||||
# Admin
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^admin/', admin.site.urls),
|
||||
|
||||
]
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from circuits.tables import CircuitSearchTable, ProviderSearchTable
|
||||
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
|
||||
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
|
||||
from dcim.tables import DeviceSearchTable, DeviceTypeSearchTable, RackSearchTable, SiteSearchTable
|
||||
from extras.models import UserAction
|
||||
from extras.models import TopologyMap, UserAction
|
||||
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||
from ipam.tables import AggregateSearchTable, IPAddressSearchTable, PrefixSearchTable, VLANSearchTable, VRFSearchTable
|
||||
@@ -148,6 +148,7 @@ def home(request):
|
||||
return render(request, 'home.html', {
|
||||
'search_form': SearchForm(),
|
||||
'stats': stats,
|
||||
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
|
||||
'recent_activity': UserAction.objects.select_related('user')[:50]
|
||||
})
|
||||
|
||||
|
||||
@@ -316,6 +316,16 @@ li.occupied + li.available {
|
||||
border-top: 1px solid #474747;
|
||||
}
|
||||
|
||||
/* Devices */
|
||||
table.component-list tr.ipaddress td {
|
||||
background-color: #eeffff;
|
||||
padding-bottom: 4px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
table.component-list tr.ipaddress:hover td {
|
||||
background-color: #e6f7f7;
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
.banner-bottom {
|
||||
margin-bottom: 50px;
|
||||
|
||||
@@ -80,7 +80,7 @@ $(document).ready(function() {
|
||||
child_field.append($("<option></option>").attr("value", "").text("---------"));
|
||||
|
||||
if ($(this).val() || $(this).attr('nullable') == 'true') {
|
||||
var api_url = child_field.attr('api-url');
|
||||
var api_url = child_field.attr('api-url') + '&limit=1000';
|
||||
var disabled_indicator = child_field.attr('disabled-indicator');
|
||||
var initial_value = child_field.attr('initial');
|
||||
var display_field = child_field.attr('display-field') || 'name';
|
||||
@@ -88,20 +88,21 @@ $(document).ready(function() {
|
||||
// Determine the filter fields needed to make an API call
|
||||
var filter_regex = /\{\{([a-z_]+)\}\}/g;
|
||||
var match;
|
||||
var rendered_url = api_url;
|
||||
while (match = filter_regex.exec(api_url)) {
|
||||
var filter_field = $('#id_' + match[1]);
|
||||
if (filter_field.val()) {
|
||||
api_url = api_url.replace(match[0], filter_field.val());
|
||||
} else if ($(this).attr('nullable') == 'true') {
|
||||
api_url = api_url.replace(match[0], '0');
|
||||
rendered_url = rendered_url.replace(match[0], filter_field.val());
|
||||
} else if (filter_field.attr('nullable') == 'true') {
|
||||
rendered_url = rendered_url.replace(match[0], '0');
|
||||
}
|
||||
}
|
||||
|
||||
// If all URL variables have been replaced, make the API call
|
||||
if (api_url.search('{{') < 0) {
|
||||
console.log(child_name + ": Fetching " + api_url);
|
||||
if (rendered_url.search('{{') < 0) {
|
||||
console.log(child_name + ": Fetching " + rendered_url);
|
||||
$.ajax({
|
||||
url: api_url,
|
||||
url: rendered_url,
|
||||
dataType: 'json',
|
||||
success: function(response, status) {
|
||||
$.each(response.results, function(index, choice) {
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
// Unlocking a secret
|
||||
$('button.unlock-secret').click(function() {
|
||||
$('button.unlock-secret').click(function(event) {
|
||||
var secret_id = $(this).attr('secret-id');
|
||||
unlock_secret(secret_id);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Locking a secret
|
||||
$('button.lock-secret').click(function() {
|
||||
$('button.lock-secret').click(function(event) {
|
||||
var secret_id = $(this).attr('secret-id');
|
||||
lock_secret(secret_id);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Adding/editing a secret
|
||||
$('form').submit(function(event) {
|
||||
if (
|
||||
$(this).find('input.requires-session-key').filter(function() {return this.value == ""}) &&
|
||||
document.cookie.indexOf('session_key') == -1
|
||||
) {
|
||||
$('#privkey_modal').modal('show');
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Retrieve a session key
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.contrib import admin, messages
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from .forms import ActivateUserKeyForm
|
||||
from .models import UserKey, SecretRole, Secret
|
||||
from .models import UserKey
|
||||
|
||||
|
||||
@admin.register(UserKey)
|
||||
@@ -10,7 +10,7 @@ class UserKeyAdmin(admin.ModelAdmin):
|
||||
actions = ['activate_selected']
|
||||
list_display = ['user', 'is_filled', 'is_active', 'created']
|
||||
fields = ['user', 'public_key', 'is_active', 'last_updated']
|
||||
readonly_fields = ['is_active', 'last_updated']
|
||||
readonly_fields = ['user', 'is_active', 'last_updated']
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
# Don't allow a user to modify an existing public key directly.
|
||||
|
||||
@@ -22,4 +22,5 @@ router.register(r'secrets', views.SecretViewSet)
|
||||
router.register(r'get-session-key', views.GetSessionKeyViewSet, base_name='get-session-key')
|
||||
router.register(r'generate-rsa-key-pair', views.GenerateRSAKeyPairViewSet, base_name='generate-rsa-key-pair')
|
||||
|
||||
app_name = 'secrets-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -48,7 +48,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class SecretForm(BootstrapMixin, forms.ModelForm):
|
||||
plaintext = forms.CharField(max_length=65535, required=False, label='Plaintext',
|
||||
widget=forms.PasswordInput())
|
||||
widget=forms.PasswordInput(attrs={'class': 'requires-session-key'}))
|
||||
plaintext2 = forms.CharField(max_length=65535, required=False, label='Plaintext (verify)',
|
||||
widget=forms.PasswordInput())
|
||||
|
||||
@@ -82,7 +82,7 @@ class SecretFromCSVForm(forms.ModelForm):
|
||||
|
||||
|
||||
class SecretImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=SecretFromCSVForm)
|
||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-session-key'}))
|
||||
|
||||
|
||||
class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.6 on 2017-03-14 17:19
|
||||
# Generated by Django 1.11 on 2017-04-27 15:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['user__username'],
|
||||
'ordering': ['userkey__user__username'],
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
|
||||
@@ -6,8 +6,8 @@ from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password, check_password
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_bytes, python_2_unicode_compatible
|
||||
|
||||
from dcim.models import Device
|
||||
@@ -69,7 +69,7 @@ class UserKey(CreatedUpdatedModel):
|
||||
copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's
|
||||
matching (private) decryption key.
|
||||
"""
|
||||
user = models.OneToOneField(User, related_name='user_key', editable=False)
|
||||
user = models.OneToOneField(User, related_name='user_key', editable=False, on_delete=models.CASCADE)
|
||||
public_key = models.TextField(verbose_name='RSA public key')
|
||||
master_key_cipher = models.BinaryField(max_length=512, blank=True, null=True, editable=False)
|
||||
|
||||
@@ -195,7 +195,7 @@ class SessionKey(models.Model):
|
||||
key = None
|
||||
|
||||
class Meta:
|
||||
ordering = ['user__username']
|
||||
ordering = ['userkey__user__username']
|
||||
|
||||
def __str__(self):
|
||||
return self.userkey.user.username
|
||||
@@ -283,7 +283,7 @@ class Secret(CreatedUpdatedModel):
|
||||
A Secret can be up to 65,536 bytes (64KB) in length. Each secret string will be padded with random data to a minimum
|
||||
of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
|
||||
"""
|
||||
device = models.ForeignKey(Device, related_name='secrets')
|
||||
device = models.ForeignKey(Device, related_name='secrets', on_delete=models.CASCADE)
|
||||
role = models.ForeignKey('SecretRole', related_name='secrets', on_delete=models.PROTECT)
|
||||
name = models.CharField(max_length=100, blank=True)
|
||||
ciphertext = models.BinaryField(editable=False, max_length=65568) # 16B IV + 2B pad length + {62-65550}B padded
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'secrets'
|
||||
urlpatterns = [
|
||||
|
||||
# Secret roles
|
||||
|
||||
@@ -3,10 +3,10 @@ import base64
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required, login_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from dcim.models import Device
|
||||
@@ -14,7 +14,17 @@ from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, Obje
|
||||
|
||||
from . import filters, forms, tables
|
||||
from .decorators import userkey_required
|
||||
from .models import SecretRole, Secret, SessionKey, UserKey
|
||||
from .models import SecretRole, Secret, SessionKey
|
||||
|
||||
|
||||
def get_session_key(request):
|
||||
"""
|
||||
Extract and decode the session key sent with a request. Returns None if no session key was provided.
|
||||
"""
|
||||
session_key = request.COOKIES.get('session_key', None)
|
||||
if session_key is not None:
|
||||
return base64.b64decode(session_key)
|
||||
return session_key
|
||||
|
||||
|
||||
#
|
||||
@@ -32,7 +42,7 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = SecretRole
|
||||
form_class = forms.SecretRoleForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('secrets:secretrole_list')
|
||||
|
||||
|
||||
@@ -73,14 +83,13 @@ def secret_add(request, pk):
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
secret = Secret(device=device)
|
||||
uk = UserKey.objects.get(user=request.user)
|
||||
session_key = get_session_key(request)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.SecretForm(request.POST, instance=secret)
|
||||
if form.is_valid():
|
||||
|
||||
# We need a valid session key in order to create a Secret
|
||||
session_key = base64.b64decode(request.COOKIES.get('session_key', None))
|
||||
if session_key is None:
|
||||
form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.")
|
||||
|
||||
@@ -119,13 +128,13 @@ def secret_add(request, pk):
|
||||
def secret_edit(request, pk):
|
||||
|
||||
secret = get_object_or_404(Secret, pk=pk)
|
||||
session_key = get_session_key(request)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.SecretForm(request.POST, instance=secret)
|
||||
if form.is_valid():
|
||||
|
||||
# Re-encrypt the Secret if a plaintext and session key have been provided.
|
||||
session_key = base64.b64decode(request.COOKIES.get('session_key', None))
|
||||
if form.cleaned_data['plaintext'] and session_key is not None:
|
||||
|
||||
# Retrieve the master key using the provided session key
|
||||
@@ -212,6 +221,7 @@ def secret_import(request):
|
||||
|
||||
return render(request, 'import_success.html', {
|
||||
'table': table,
|
||||
'return_url': 'secrets:secret_list',
|
||||
})
|
||||
|
||||
except IntegrityError as e:
|
||||
@@ -222,7 +232,7 @@ def secret_import(request):
|
||||
|
||||
return render(request, 'secrets/secret_import.html', {
|
||||
'form': form,
|
||||
'return_url': reverse('secrets:secret_list'),
|
||||
'return_url': 'secrets:secret_list',
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
|
||||
<li><a href="{% url 'dcim:rack_elevation_list' %}"><i class="fa fa-bars" aria-hidden="true"></i> Rack Elevations</a></li>
|
||||
{% if perms.dcim.add_rack %}
|
||||
<li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li>
|
||||
<li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>
|
||||
@@ -72,6 +73,8 @@
|
||||
{% if perms.dcim.add_rackrole %}
|
||||
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:rackreservation_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Reservations</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}">
|
||||
@@ -240,16 +243,6 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" role="search">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="dropdown">
|
||||
@@ -269,13 +262,23 @@
|
||||
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" role="search">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container wrapper">
|
||||
<div class="container wrapper">
|
||||
{% if settings.BANNER_TOP %}
|
||||
<div class="alert alert-info text-center" role="alert">
|
||||
{{ settings.BANNER_TOP|safe }}
|
||||
{{ settings.BANNER_TOP|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if settings.MAINTENANCE_MODE %}
|
||||
@@ -284,24 +287,24 @@
|
||||
<p>NetBox is currently in maintenance mode. Functionality may be limited.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissable" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
{{ message|safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block content %}{% endblock %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissable" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block content %}{% endblock %}
|
||||
<div class="push"></div>
|
||||
{% if settings.BANNER_BOTTOM %}
|
||||
<div class="alert alert-info text-center banner-bottom" role="alert">
|
||||
{% if settings.BANNER_BOTTOM %}
|
||||
<div class="alert alert-info text-center banner-bottom" role="alert">
|
||||
{{ settings.BANNER_BOTTOM|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<p class="text-muted">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</p>
|
||||
@@ -312,14 +315,14 @@
|
||||
<div class="col-xs-4 text-right">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> ·
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> ·
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> ·
|
||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a> ·
|
||||
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="text/javascript">
|
||||
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||
</script>
|
||||
|
||||
@@ -1,72 +1,57 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Circuit Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Circuit Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Circuit ID</td>
|
||||
<td>Alphanumeric circuit identifier</td>
|
||||
<td>IC-603122</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>Name of circuit provider</td>
|
||||
<td>TeliaSonera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>Circuit type</td>
|
||||
<td>Transit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Strickland Propane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Install Date</td>
|
||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
||||
<td>2016-02-23</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit rate</td>
|
||||
<td>Commited rate in Kbps (optional)</td>
|
||||
<td>2000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Primary for voice</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Circuit ID</td>
|
||||
<td>Alphanumeric circuit identifier</td>
|
||||
<td>IC-603122</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>Name of circuit provider</td>
|
||||
<td>TeliaSonera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>Circuit type</td>
|
||||
<td>Transit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Strickland Propane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Install Date</td>
|
||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
||||
<td>2016-02-23</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit rate</td>
|
||||
<td>Commited rate in Kbps (optional)</td>
|
||||
<td>2000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Primary for voice</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,62 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Provider Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Provider Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Provider's proper name</td>
|
||||
<td>Level 3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>URL-friendly name</td>
|
||||
<td>level3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ASN</td>
|
||||
<td>Autonomous system number (optional)</td>
|
||||
<td>3356</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Account</td>
|
||||
<td>Account number (optional)</td>
|
||||
<td>08931544</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Portal URL</td>
|
||||
<td>Customer service portal URL (optional)</td>
|
||||
<td>https://mylevel3.net</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>Level 3,level3,3356,08931544,https://mylevel3.net</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Provider's proper name</td>
|
||||
<td>Level 3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>URL-friendly name</td>
|
||||
<td>level3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ASN</td>
|
||||
<td>Autonomous system number (optional)</td>
|
||||
<td>3356</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Account</td>
|
||||
<td>Account number (optional)</td>
|
||||
<td>08931544</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Portal URL</td>
|
||||
<td>Customer service portal URL (optional)</td>
|
||||
<td>https://mylevel3.net</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>Level 3,level3,3356,08931544,https://mylevel3.net</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,61 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Console Connections Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Console Connections Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Console server</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-cs3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Console server port</td>
|
||||
<td>Full CS port name</td>
|
||||
<td>Port 35</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Console Port</td>
|
||||
<td>Console port name</td>
|
||||
<td>Console</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>planned</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-cs3,Port 35,abc1-switch7,Console,planned</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Console server</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-cs3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Console server port</td>
|
||||
<td>Full CS port name</td>
|
||||
<td>Port 35</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Console Port</td>
|
||||
<td>Console port name</td>
|
||||
<td>Console</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>planned</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-cs3,Port 35,abc1-switch7,Console,planned</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -123,11 +123,7 @@
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
{% if device.status %}
|
||||
<span class="label label-success">{{ device.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-danger">{{ device.get_status_display }}</span>
|
||||
{% endif %}
|
||||
<span class="label label-{{ device.get_status_class }}">{{ device.get_status_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -194,35 +190,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>IP Addresses</strong>
|
||||
</div>
|
||||
{% if ip_addresses %}
|
||||
<table class="table table-hover panel-body">
|
||||
{% for ip in ip_addresses %}
|
||||
{% include 'dcim/inc/ipaddress.html' %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% elif interfaces or mgmt_interfaces %}
|
||||
<div class="panel-body text-muted">
|
||||
None assigned
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel-body">
|
||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}">Create an interface</a> to assign an IP.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
{% if interfaces or mgmt_interfaces %}
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign IP address
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Services</strong>
|
||||
@@ -250,7 +217,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Critical Connections</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for iface in mgmt_interfaces %}
|
||||
{% include 'dcim/inc/interface.html' with icon='wrench' %}
|
||||
{% empty %}
|
||||
@@ -389,7 +356,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for devicebay in device_bays %}
|
||||
{% include 'dcim/inc/devicebay.html' with selectable=True %}
|
||||
{% empty %}
|
||||
@@ -430,6 +397,9 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle-ips" selected="selected">
|
||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
|
||||
</button>
|
||||
{% if perms.dcim.change_interface and interfaces|length > 1 %}
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
@@ -442,7 +412,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table id="interfaces_table" class="table table-hover panel-body component-list">
|
||||
{% for iface in interfaces %}
|
||||
{% include 'dcim/inc/interface.html' with selectable=True %}
|
||||
{% empty %}
|
||||
@@ -499,7 +469,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for csp in cs_ports %}
|
||||
{% include 'dcim/inc/consoleserverport.html' with selectable=True %}
|
||||
{% empty %}
|
||||
@@ -551,7 +521,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for po in power_outlets %}
|
||||
{% include 'dcim/inc/poweroutlet.html' with selectable=True %}
|
||||
{% empty %}
|
||||
@@ -642,6 +612,18 @@ $(".powerport-toggle").click(function() {
|
||||
$(".interface-toggle").click(function() {
|
||||
return toggleConnection($(this), "dcim/interface-connections/");
|
||||
});
|
||||
// Toggle the display of IP addresses under interfaces
|
||||
$('button.toggle-ips').click(function() {
|
||||
var selected = $(this).attr('selected');
|
||||
if (selected) {
|
||||
$('#interfaces_table tr.ipaddress').hide();
|
||||
} else {
|
||||
$('#interfaces_table tr.ipaddress').show();
|
||||
}
|
||||
$(this).attr('selected', !selected);
|
||||
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
|
||||
<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Management</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.platform %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.platform %}
|
||||
{% if obj.pk %}
|
||||
{% render_field form.primary_ip4 %}
|
||||
{% render_field form.primary_ip6 %}
|
||||
|
||||
@@ -12,8 +12,12 @@
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
<div class="col-md-12 text-right">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% if return_url %}
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h4>CSV Format</h4>
|
||||
@@ -66,6 +70,11 @@
|
||||
<td>Unique alphanumeric tag (optional)</td>
|
||||
<td>ABC123456</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Site name</td>
|
||||
@@ -89,7 +98,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,ABC123456,Ashburn-VA,R101,21,Rear</pre>
|
||||
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,ABC123456,Active,Ashburn-VA,R101,21,Rear</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,8 +12,12 @@
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
<div class="col-md-12 text-right">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% if return_url %}
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h4>CSV Format</h4>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr{% if cp.cs_port and not cp.connection_status %} class="info"{% endif %}>
|
||||
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
|
||||
{% if selectable and perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
|
||||
@@ -20,7 +20,7 @@
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
{% if cp.cs_port %}
|
||||
{% if cp.connection_status %}
|
||||
@@ -33,11 +33,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" class="btn btn-info btn-xs">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr{% if csp.connected_console and not csp.connected_console.connection_status %} class="info"{% endif %}>
|
||||
<tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}">
|
||||
{% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ csp.pk }}" />
|
||||
@@ -7,6 +7,7 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if csp.connected_console %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
|
||||
@@ -19,7 +20,7 @@
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_consoleserverport %}
|
||||
{% if csp.connected_console %}
|
||||
{% if csp.connected_console.connection_status %}
|
||||
@@ -32,11 +33,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" class="btn btn-info btn-xs">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr>
|
||||
<tr class="devicebay">
|
||||
{% if selectable and perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="text-muted">Vacant</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_devicebay %}
|
||||
{% if devicebay.installed_device %}
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=devicebay.pk %}" class="btn btn-danger btn-xs">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr{% if iface.connection and not iface.connection.connection_status %} class="info"{% endif %}>
|
||||
<tr class="interface{% if iface.connection and not iface.connection.connection_status %} info{% endif %}">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||
@@ -12,14 +12,14 @@
|
||||
{% if iface.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
||||
{% endif %}
|
||||
{% if iface.is_lag %}
|
||||
<br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<small>{{ iface.mac_address|default:'' }}</small>
|
||||
</td>
|
||||
{% if iface.is_virtual %}
|
||||
<td>{{ iface.mac_address|default:"" }}</td>
|
||||
{% if iface.is_lag %}
|
||||
<td colspan="2" class="text-muted">
|
||||
LAG interface<br />
|
||||
<small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
||||
</td>
|
||||
{% elif iface.is_virtual %}
|
||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||
{% elif iface.connection %}
|
||||
{% with iface.connected_interface as connected_iface %}
|
||||
@@ -51,7 +51,7 @@
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if show_graphs %}
|
||||
{% if iface.circuit_termination or iface.connection %}
|
||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
|
||||
@@ -59,6 +59,11 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?interface_site={{ device.site.pk }}&interface_rack={{ device.rack.pk }}&interface_device={{ device.pk }}&interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_interface %}
|
||||
{% if not iface.is_virtual %}
|
||||
{% if iface.connection %}
|
||||
@@ -71,19 +76,19 @@
|
||||
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Disconnect">
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% elif iface.circuit_termination and perms.circuits.change_circuittermination %}
|
||||
<button class="btn btn-warning btn-xs interface-toggle connected" disabled="disabled" title="Circuits cannot be marked as planned or connected">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</button>
|
||||
<a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}" class="btn btn-danger btn-xs" title="Edit circuit termination">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface_a={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -104,3 +109,41 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% for ip in iface.ip_addresses.all %}
|
||||
<tr class="ipaddress">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td colspan="2">
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
{% if ip.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||
{% endif %}
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if ip.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Global</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.edit_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<tr>
|
||||
<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 %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr{% if po.connected_port and not po.connected_port.connection_status %} class="info"{% endif %}>
|
||||
<tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}">
|
||||
{% if selectable and perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ po.pk }}" />
|
||||
@@ -7,6 +7,7 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ po.name }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if po.connected_port %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
|
||||
@@ -19,7 +20,7 @@
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_poweroutlet %}
|
||||
{% if po.connected_port %}
|
||||
{% if po.connected_port.connection_status %}
|
||||
@@ -32,11 +33,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" class="btn btn-info btn-xs">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr{% if pp.power_outlet and not pp.connection_status %} class="info"{% endif %}>
|
||||
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
|
||||
{% if selectable and perms.dcim.change_powerport or perms.dcim.delete_powerport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ pp.pk }}" />
|
||||
@@ -20,7 +20,7 @@
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_powerport %}
|
||||
{% if pp.power_outlet %}
|
||||
{% if pp.connection_status %}
|
||||
@@ -33,11 +33,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" class="btn btn-info btn-xs">
|
||||
|
||||
@@ -1,69 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Interface Connections Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Interface Connections Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Device A</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-core1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface A</td>
|
||||
<td>Interface name</td>
|
||||
<td>xe-0/0/6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device B</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface B</td>
|
||||
<td>Interface name</td>
|
||||
<td>xe-0/0/0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>planned</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Device A</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-core1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface A</td>
|
||||
<td>Interface name</td>
|
||||
<td>xe-0/0/6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device B</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface B</td>
|
||||
<td>Interface name</td>
|
||||
<td>xe-0/0/0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>planned</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,61 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Power Connections Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Power Connections Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>PDU</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-pdu1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Outlet</td>
|
||||
<td>Power outlet name</td>
|
||||
<td>AC4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Port</td>
|
||||
<td>Power port name</td>
|
||||
<td>PSU0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>connected</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-pdu1,AC4,abc1-switch7,PSU0,connected</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>PDU</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-pdu1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Outlet</td>
|
||||
<td>Power outlet name</td>
|
||||
<td>AC4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Port</td>
|
||||
<td>Power port name</td>
|
||||
<td>PSU0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>connected</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-pdu1,AC4,abc1-switch7,PSU0,connected</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
</tr>
|
||||
{% for resv in reservations %}
|
||||
<tr>
|
||||
<td>{{ resv.units|join:', ' }}</td>
|
||||
<td>{{ resv.unit_list }}</td>
|
||||
<td>
|
||||
{{ resv.description }}<br />
|
||||
<small>{{ resv.user }} · {{ resv.created }}</small>
|
||||
|
||||
50
netbox/templates/dcim/rack_elevation_list.html
Normal file
50
netbox/templates/dcim/rack_elevation_list.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="btn-group pull-right" role="group">
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=0 %}" class="btn btn-default{% if request.GET.face != '1' %} active{% endif %}">Front</a>
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=1 %}" class="btn btn-default{% if request.GET.face == '1' %} active{% endif %}">Rear</a>
|
||||
</div>
|
||||
<h1>{% block title %}Rack Elevations{% endblock %}</h1>
|
||||
<div class="row">
|
||||
{% if page %}
|
||||
<div class="col-md-9">
|
||||
<div style="white-space: nowrap; overflow-x: scroll;">
|
||||
{% for rack in page %}
|
||||
<div style="display: inline-block; width: 266px">
|
||||
<div class="rack_header">
|
||||
<h4>{{ rack.name }}</h4>
|
||||
</div>
|
||||
{% if face_id %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
|
||||
{% else %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="rack_header">
|
||||
<h4>{{ rack.name }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include 'paginator.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-md-9">
|
||||
<p>No racks found</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$('[data-toggle="popover"]').popover()
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,87 +1,72 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Rack Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Rack Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Name of the assigned site</td>
|
||||
<td>DC-4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Rack group name (optional)</td>
|
||||
<td>Cage 1400</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Internal rack name</td>
|
||||
<td>R101</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Facility ID</td>
|
||||
<td>Rack ID assigned by the facility (optional)</td>
|
||||
<td>J12.100</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Pied Piper</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Compute</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>Rack type (optional)</td>
|
||||
<td>4-post cabinet</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Width</td>
|
||||
<td>Rail-to-rail width (19 or 23 inches)</td>
|
||||
<td>19</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Height</td>
|
||||
<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,False</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Name of the assigned site</td>
|
||||
<td>DC-4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Rack group name (optional)</td>
|
||||
<td>Cage 1400</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Internal rack name</td>
|
||||
<td>R101</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Facility ID</td>
|
||||
<td>Rack ID assigned by the facility (optional)</td>
|
||||
<td>J12.100</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Pied Piper</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Compute</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>Rack type (optional)</td>
|
||||
<td>4-post cabinet</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Width</td>
|
||||
<td>Rail-to-rail width (19 or 23 inches)</td>
|
||||
<td>19</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Height</td>
|
||||
<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,False</pre>
|
||||
{% endblock %}
|
||||
|
||||
14
netbox/templates/dcim/rackreservation_list.html
Normal file
14
netbox/templates/dcim/rackreservation_list.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% block title %}Rack Reservations{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackreservation_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -224,7 +224,7 @@
|
||||
<table class="table table-hover panel-body">
|
||||
{% for rg in rack_groups %}
|
||||
<tr>
|
||||
<td><i class="fa fa-fw fa-folder"></i> <a href="{{ rg.get_absolute_url }}">{{ rg.name }}</a></td>
|
||||
<td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg.name }}</a></td>
|
||||
<td>{{ rg.rack_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -257,7 +257,7 @@
|
||||
<table class="table table-hover panel-body">
|
||||
{% for tm in topology_maps %}
|
||||
<tr>
|
||||
<td><i class="fa fa-fw fa-map"></i> <a href="{% url 'extras-api:topologymap-render' pk=tm.pk %}" target="_blank">{{ tm }}</a></td>
|
||||
<td><i class="fa fa-fw fa-map-o"></i> <a href="{% url 'extras-api:topologymap-render' pk=tm.pk %}" target="_blank">{{ tm }}</a></td>
|
||||
<td>{{ tm.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -48,6 +48,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.secrets %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Secrets</strong>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<span class="badge pull-right">{{ stats.secret_count }}</span>
|
||||
<h4 class="list-group-item-heading"><a href="{% url 'secrets:secret_list' %}">Secrets</a></h4>
|
||||
<p class="list-group-item-text text-muted">Sensitive data (such as passwords) which has been stored securely</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="panel panel-default">
|
||||
@@ -101,20 +115,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4">
|
||||
{% if perms.secrets %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Secrets</strong>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<span class="badge pull-right">{{ stats.secret_count }}</span>
|
||||
<h4 class="list-group-item-heading"><a href="{% url 'secrets:secret_list' %}">Secrets</a></h4>
|
||||
<p class="list-group-item-text text-muted">Sensitive data (such as passwords) which has been stored securely</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Global Topology Maps</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if topology_maps %}
|
||||
<table class="table table-hover panel-body">
|
||||
{% for tm in topology_maps %}
|
||||
<tr>
|
||||
<td><i class="fa fa-fw fa-map-o"></i> <a href="{% url 'extras-api:topologymap-render' pk=tm.pk %}" target="_blank">{{ tm }}</a></td>
|
||||
<td>{{ tm.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
None
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Recent Activity</strong>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}Import Completed{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Import Completed</h1>
|
||||
{% render_table table %}
|
||||
<a href="{{ request.path }}" class="btn btn-primary">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import more
|
||||
</a>
|
||||
<h1>{% block title %}Import Completed{% endblock %}</h1>
|
||||
{% render_table table %}
|
||||
<a href="{{ request.path }}" class="btn btn-primary">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import more
|
||||
</a>
|
||||
{% if return_url %}
|
||||
<a href="{% url return_url %}" class="btn btn-default">View All</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for attachment in images %}
|
||||
<tr>
|
||||
<tr{% if not attachment.size %} class="danger"{% endif %}>
|
||||
<td>
|
||||
<i class="fa fa-image"></i>
|
||||
<a href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
|
||||
</td>
|
||||
<td>{{ attachment.image.size|filesizeformat }}</td>
|
||||
<td>{{ attachment.size|filesizeformat }}</td>
|
||||
<td>{{ attachment.created }}</td>
|
||||
<td class="text-right">
|
||||
{% if perms.extras.change_imageattachment %}
|
||||
|
||||
@@ -1,57 +1,42 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Aggregate Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Aggregate Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prefix</td>
|
||||
<td>IPv4 or IPv6 network</td>
|
||||
<td>172.16.0.0/12</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RIR</td>
|
||||
<td>Name of RIR</td>
|
||||
<td>RFC 1918</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date Added</td>
|
||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
||||
<td>2016-02-23</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Private IPv4 space</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prefix</td>
|
||||
<td>IPv4 or IPv6 network</td>
|
||||
<td>172.16.0.0/12</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RIR</td>
|
||||
<td>Name of RIR</td>
|
||||
<td>RFC 1918</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date Added</td>
|
||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
||||
<td>2016-02-23</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Private IPv4 space</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -98,14 +98,8 @@
|
||||
<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>
|
||||
|
||||
@@ -10,13 +10,21 @@
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>IP Address</strong></div>
|
||||
<div class="panel-heading"><strong>IP Addresses</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.address %}
|
||||
{% render_field form.address_pattern %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -16,39 +16,21 @@
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% if obj.pk %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Device</label>
|
||||
<div class="col-md-9">
|
||||
<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 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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Interface</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
{% if obj.interface %}
|
||||
{{ obj.interface }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interface Assignment</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.interface_site %}
|
||||
{% render_field form.interface_rack %}
|
||||
{% render_field form.interface_device %}
|
||||
{% render_field form.interface %}
|
||||
{% render_field form.primary_for_device %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>NAT IP (Inside)</strong></div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -1,77 +1,62 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}IP Address Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>IP Address Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Address</td>
|
||||
<td>IPv4 or IPv6 address</td>
|
||||
<td>192.0.2.42/24</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VRF</td>
|
||||
<td>VRF route distinguisher (optional)</td>
|
||||
<td>65000:123</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<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>
|
||||
<td>switch12</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface</td>
|
||||
<td>Interface name (optional)</td>
|
||||
<td>ge-0/0/31</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is Primary</td>
|
||||
<td>If "true", IP will be primary for device (optional)</td>
|
||||
<td>True</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Management IP</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Address</td>
|
||||
<td>IPv4 or IPv6 address</td>
|
||||
<td>192.0.2.42/24</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VRF</td>
|
||||
<td>VRF route distinguisher (optional)</td>
|
||||
<td>65000:123</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<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>
|
||||
<td>switch12</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface</td>
|
||||
<td>Interface name (optional)</td>
|
||||
<td>ge-0/0/31</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is Primary</td>
|
||||
<td>If "true", IP will be primary for device (optional)</td>
|
||||
<td>True</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Management IP</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,87 +1,72 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Prefix Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Prefix Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prefix</td>
|
||||
<td>IPv4 or IPv6 network</td>
|
||||
<td>192.168.42.0/24</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VRF</td>
|
||||
<td>VRF route distinguisher (optional)</td>
|
||||
<td>65000:123</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>ABC01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Name of assigned site (optional)</td>
|
||||
<td>HQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VLAN Group</td>
|
||||
<td>Name of group for VLAN selection (optional)</td>
|
||||
<td>Customers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VLAN ID</td>
|
||||
<td>Numeric VLAN ID (optional)</td>
|
||||
<td>801</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Customer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is a pool</td>
|
||||
<td>True if all IPs are considered usable</td>
|
||||
<td>False</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>7th floor WiFi</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prefix</td>
|
||||
<td>IPv4 or IPv6 network</td>
|
||||
<td>192.168.42.0/24</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VRF</td>
|
||||
<td>VRF route distinguisher (optional)</td>
|
||||
<td>65000:123</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>ABC01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Name of assigned site (optional)</td>
|
||||
<td>HQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VLAN Group</td>
|
||||
<td>Name of group for VLAN selection (optional)</td>
|
||||
<td>Customers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VLAN ID</td>
|
||||
<td>Numeric VLAN ID (optional)</td>
|
||||
<td>801</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Customer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is a pool</td>
|
||||
<td>True if all IPs are considered usable</td>
|
||||
<td>False</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>7th floor WiFi</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,15 +6,10 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'ipam:prefix_list' %}{% querystring_toggle request expand='on' %}" class="btn btn-default">
|
||||
{% if 'expand' in request.GET %}
|
||||
<span class="fa fa-chevron-right" aria-hidden="true"></span>
|
||||
Collapse all
|
||||
{% else %}
|
||||
<span class="fa fa-chevron-down" aria-hidden="true"></span>
|
||||
Expand all
|
||||
{% endif %}
|
||||
</a>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand=None page=1 %}" class="btn btn-default{% if not request.GET.expand %} active{% endif %}">Collapse</a>
|
||||
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
|
||||
</div>
|
||||
{% if perms.ipam.add_prefix %}
|
||||
<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
|
||||
@@ -1,77 +1,62 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}VLAN Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>VLAN Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Name of assigned site</td>
|
||||
<td>LAS2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Name of VLAN group (optional)</td>
|
||||
<td>Backend Network</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>Configured VLAN ID</td>
|
||||
<td>1400</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Configured VLAN name</td>
|
||||
<td>Cameras</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Internal</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Security</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Security team only</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Name of assigned site</td>
|
||||
<td>LAS2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Name of VLAN group (optional)</td>
|
||||
<td>Backend Network</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>Configured VLAN ID</td>
|
||||
<td>1400</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Configured VLAN name</td>
|
||||
<td>Cameras</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Internal</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Security</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Security team only</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,62 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}VRF Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>VRF Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Name of VRF</td>
|
||||
<td>Customer_ABC</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RD</td>
|
||||
<td>Route distinguisher</td>
|
||||
<td>65000:123456</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>ABC01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enforce uniqueness</td>
|
||||
<td>Prevent duplicate prefixes/IP addresses</td>
|
||||
<td>True</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Native VRF for customer ABC</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Name of VRF</td>
|
||||
<td>Customer_ABC</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RD</td>
|
||||
<td>Route distinguisher</td>
|
||||
<td>65000:123456</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>ABC01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enforce uniqueness</td>
|
||||
<td>Prevent duplicate prefixes/IP addresses</td>
|
||||
<td>True</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Native VRF for customer ABC</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
{% load django_tables2 %}
|
||||
{% load helpers %}
|
||||
|
||||
{# Custom pagination controls to render nicely with Bootstrap CSS. smart_pages requires EnhancedPaginator. #}
|
||||
|
||||
<div class="paginator pull-right">
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
<div class="paginator pull-right" style="margin-top: 20px">
|
||||
{% if paginator.num_pages > 1 %}
|
||||
<nav>
|
||||
<ul class="pagination pull-right">
|
||||
{% if table.page.has_previous %}
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">«</a></li>
|
||||
{% if page.has_previous %}
|
||||
<li><a href="{% querystring request page=page.previous_page_number %}">«</a></li>
|
||||
{% endif %}
|
||||
{% for p in table.page.smart_pages %}
|
||||
{% for p in page.smart_pages %}
|
||||
{% if p %}
|
||||
<li{% ifequal table.page.number p %} class="active"{% endifequal %}><a href="{% querystring table.prefixed_page_field=p %}">{{ p }}</a></li>
|
||||
<li{% ifequal page.number p %} class="active"{% endifequal %}><a href="{% querystring request page=p %}">{{ p }}</a></li>
|
||||
{% else %}
|
||||
<li class="disabled"><span>…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if table.page.has_next %}
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">»</a></li>
|
||||
{% if page.has_next %}
|
||||
<li><a href="{% querystring request page=page.next_page_number %}">»</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="text-right text-muted">
|
||||
Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
|
||||
{% if total == 1 %}
|
||||
{{ table.data.verbose_name }}
|
||||
{% else %}
|
||||
{{ table.data.verbose_name_plural }}
|
||||
{% endif %}
|
||||
Showing {{ page.start_index }}-{{ page.end_index }} of {{ total_count }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
|
||||
{% block pagination %}
|
||||
{% if not hide_paginator %}
|
||||
{% include 'paginator.html' %}
|
||||
{% include 'table_paginator.html' %}
|
||||
{% endif %}
|
||||
{% endblock pagination %}
|
||||
|
||||
@@ -39,8 +39,16 @@
|
||||
{% if secret.pk %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Current Plaintext</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">********</p>
|
||||
<div class="col-md-7">
|
||||
<p class="form-control-static" id="secret_{{ secret.pk }}">********</p>
|
||||
</div>
|
||||
<div class="col-md-2 text-right">
|
||||
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
|
||||
<i class="fa fa-lock"></i> Unlock
|
||||
</button>
|
||||
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
|
||||
<i class="fa fa-unlock-alt"></i> Lock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -20,10 +20,14 @@
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-12 text-right">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% if return_url %}
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{% extends 'django_tables2/table.html' %}
|
||||
{% extends 'django_tables2/bootstrap-responsive.html' %}
|
||||
{% load django_tables2 %}
|
||||
|
||||
{# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #}
|
||||
|
||||
{% block pagination %}
|
||||
{% if not hide_paginator %}
|
||||
{% include 'paginator.html' %}
|
||||
{% include 'table_paginator.html' %}
|
||||
{% endif %}
|
||||
{% endblock pagination %}
|
||||
|
||||
34
netbox/templates/table_paginator.html
Normal file
34
netbox/templates/table_paginator.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% load django_tables2 %}
|
||||
|
||||
{# Custom pagination controls to render nicely with Bootstrap CSS. smart_pages requires EnhancedPaginator. #}
|
||||
|
||||
<div class="paginator pull-right">
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
<nav>
|
||||
<ul class="pagination pull-right">
|
||||
{% if table.page.has_previous %}
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">«</a></li>
|
||||
{% endif %}
|
||||
{% for p in table.page.smart_pages %}
|
||||
{% if p %}
|
||||
<li{% ifequal table.page.number p %} class="active"{% endifequal %}><a href="{% querystring table.prefixed_page_field=p %}">{{ p }}</a></li>
|
||||
{% else %}
|
||||
<li class="disabled"><span>…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if table.page.has_next %}
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">»</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="text-right text-muted">
|
||||
Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
|
||||
{% if total == 1 %}
|
||||
{{ table.data.verbose_name }}
|
||||
{% else %}
|
||||
{{ table.data.verbose_name_plural }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,57 +1,42 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Tenant Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Tenant Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Tenant name</td>
|
||||
<td>WIDG01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>URL-friendly name</td>
|
||||
<td>widg01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Tenant group (optional)</td>
|
||||
<td>Customers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Long-form name or other text (optional)</td>
|
||||
<td>Widgets Inc.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>WIDG01,widg01,Customers,Widgets Inc.</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Tenant name</td>
|
||||
<td>WIDG01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>URL-friendly name</td>
|
||||
<td>widg01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Tenant group (optional)</td>
|
||||
<td>Customers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Long-form name or other text (optional)</td>
|
||||
<td>Widgets Inc.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>WIDG01,widg01,Customers,Widgets Inc.</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="panel panel-{{ panel_class|default:"danger" }}">
|
||||
<div class="panel-heading">{% block title %}{% endblock %}</div>
|
||||
<div class="panel-body">
|
||||
{% block message %}<p>Are you sure?</p>{% endblock %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="form-group">
|
||||
<div class="checkbox{% if form.confirm.errors %} has-error{% endif %}">
|
||||
<label for="{{ form.confirm.id_for_label }}">
|
||||
|
||||
34
netbox/templates/utilities/obj_import.html
Normal file
34
netbox/templates/utilities/obj_import.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% block title %}{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-12 text-right">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% if return_url %}
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% block instructions %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -43,8 +43,8 @@ class NestedTenantSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class WritableTenantSerializer(serializers.ModelSerializer):
|
||||
class WritableTenantSerializer(CustomFieldModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ['id', 'name', 'slug', 'group', 'description', 'comments']
|
||||
fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields']
|
||||
|
||||
@@ -18,4 +18,5 @@ router.APIRootView = TenancyRootView
|
||||
router.register(r'tenant-groups', views.TenantGroupViewSet)
|
||||
router.register(r'tenants', views.TenantViewSet)
|
||||
|
||||
app_name = 'tenancy-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user