mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-28 20:58:15 +01:00
Compare commits
369 Commits
v2.0-beta1
...
v2.0.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a34ced72 | ||
|
|
d0dc505220 | ||
|
|
2f32e11f53 | ||
|
|
280f55a875 | ||
|
|
6f37e97c67 | ||
|
|
e54c74d972 | ||
|
|
af9fa85cc1 | ||
|
|
dc77400ab1 | ||
|
|
5f66893038 | ||
|
|
e05d379101 | ||
|
|
41ea433e7c | ||
|
|
bfd7881b7b | ||
|
|
b253c8cc95 | ||
|
|
0fc9ed852e | ||
|
|
175c1f2720 | ||
|
|
a355783377 | ||
|
|
dafdbc9ddb | ||
|
|
14f5204548 | ||
|
|
5233463f0b | ||
|
|
1d4a416100 | ||
|
|
25ee796d5b | ||
|
|
e08107063a | ||
|
|
cd5a86bfcf | ||
|
|
97b67d0f93 | ||
|
|
3f82be7192 | ||
|
|
adfcb5f7b6 | ||
|
|
5aba1d9aec | ||
|
|
afdf5750b5 | ||
|
|
ea869d4ffc | ||
|
|
9d89eed873 | ||
|
|
c00eea7991 | ||
|
|
88239e0b0d | ||
|
|
9930e2745f | ||
|
|
da3879e928 | ||
|
|
7195b7c803 | ||
|
|
9b082eea14 | ||
|
|
a16218b311 | ||
|
|
29a71fd903 | ||
|
|
fcacac7c6f | ||
|
|
78d74261e9 | ||
|
|
16d694734b | ||
|
|
252ab0fbab | ||
|
|
8eb9c451a1 | ||
|
|
469c52be28 | ||
|
|
54fa51eeff | ||
|
|
5456af6867 | ||
|
|
180446c34d | ||
|
|
5c63a499d5 | ||
|
|
3a2c5b318a | ||
|
|
cfff69a715 | ||
|
|
08883d86ef | ||
|
|
8a849ebeff | ||
|
|
05a796faf1 | ||
|
|
9e1d03b383 | ||
|
|
0a929f2971 | ||
|
|
7878992570 | ||
|
|
4f95926cbd | ||
|
|
f3e997ea39 | ||
|
|
2b921c21ff | ||
|
|
50496b1a59 | ||
|
|
9736d63577 | ||
|
|
13add414c4 | ||
|
|
b032bc13db | ||
|
|
aaad428438 | ||
|
|
203895fc7e | ||
|
|
aab1fab445 | ||
|
|
e06221bc89 | ||
|
|
26a13edcf3 | ||
|
|
65b6fe576f | ||
|
|
4671829ad8 | ||
|
|
293be752ca | ||
|
|
0a6e4f31d5 | ||
|
|
e6c4ce51f7 | ||
|
|
3924063060 | ||
|
|
d122f9f700 | ||
|
|
d0649ba815 | ||
|
|
1ec09270a7 | ||
|
|
1ddd7415cb | ||
|
|
ec9d0d4008 | ||
|
|
08c8bd3049 | ||
|
|
2520d9f400 | ||
|
|
0e863ff9ca | ||
|
|
1b78f54c6b | ||
|
|
b732c24ec4 | ||
|
|
af604aba31 | ||
|
|
c82658440f | ||
|
|
7e660d4d8e | ||
|
|
4a8147f8a5 | ||
|
|
583830c652 | ||
|
|
95fdb549d7 | ||
|
|
a598f0e632 | ||
|
|
293dbd8a8b | ||
|
|
f03a378ce0 | ||
|
|
6aae8aee5b | ||
|
|
6d908d3e79 | ||
|
|
d5016c7133 | ||
|
|
b5a1b692bd | ||
|
|
834c396a22 | ||
|
|
bc18d241e8 | ||
|
|
f7b0d22f86 | ||
|
|
5a1877087f | ||
|
|
50462ec15d | ||
|
|
1dd5e2c926 | ||
|
|
ebddc46bc0 | ||
|
|
138cbf9761 | ||
|
|
f21c6bca00 | ||
|
|
9aad8a7774 | ||
|
|
68b6c7d886 | ||
|
|
1c489e57cc | ||
|
|
6719578f14 | ||
|
|
d5587de316 | ||
|
|
77f28e3441 | ||
|
|
3fa63b774e | ||
|
|
713c7cd8e3 | ||
|
|
e6b4d87939 | ||
|
|
27c94d9874 | ||
|
|
eece8a0e26 | ||
|
|
fb85867d72 | ||
|
|
c454bfcd84 | ||
|
|
ad95b86fdd | ||
|
|
769232f368 | ||
|
|
9cf10eecd1 | ||
|
|
f927d5b8f5 | ||
|
|
7fa696dace | ||
|
|
feac93389c | ||
|
|
f7969d91b3 | ||
|
|
92aafb9043 | ||
|
|
f9328d53b4 | ||
|
|
f1cbc7da33 | ||
|
|
01becd21de | ||
|
|
7768b94279 | ||
|
|
3bc51c8e69 | ||
|
|
d206be91d5 | ||
|
|
6e69c9e375 | ||
|
|
f2846af4ec | ||
|
|
657eed1dc9 | ||
|
|
e351ab0171 | ||
|
|
779446da64 | ||
|
|
2ff0d7aa83 | ||
|
|
7ceb64b57b | ||
|
|
43e1e0dbc8 | ||
|
|
a1c12cfd77 | ||
|
|
aa6ca21a34 | ||
|
|
a49521d683 | ||
|
|
3be6e5b015 | ||
|
|
ca1725b98c | ||
|
|
d11dfe2ced | ||
|
|
ab30ba1e1b | ||
|
|
7f23cb9bf5 | ||
|
|
c9d3cf301e | ||
|
|
67282882fa | ||
|
|
73bf4f45c3 | ||
|
|
66ae62fb91 | ||
|
|
8bae804508 | ||
|
|
d87acc97c3 | ||
|
|
f9b2c59974 | ||
|
|
a870a3b918 | ||
|
|
008ed34553 | ||
|
|
e239045688 | ||
|
|
ed80bfaf02 | ||
|
|
473b35f9a3 | ||
|
|
45bb7eec0b | ||
|
|
58bb029666 | ||
|
|
0f97478b55 | ||
|
|
9efa70a551 | ||
|
|
ed65721085 | ||
|
|
83688fceb7 | ||
|
|
088f75ba0c | ||
|
|
188cfa08a9 | ||
|
|
f731900e2f | ||
|
|
b89bd24bed | ||
|
|
effda88b51 | ||
|
|
3844f70a4d | ||
|
|
8e333757f9 | ||
|
|
0fb12bcc9c | ||
|
|
44d78ef92a | ||
|
|
ebb6729a26 | ||
|
|
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 | ||
|
|
5ff4e3b194 | ||
|
|
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 | ||
|
|
1a34830f0e | ||
|
|
f000df1e15 | ||
|
|
78b0072051 | ||
|
|
178f7b4643 | ||
|
|
f34a8fff6a | ||
|
|
7766e1f684 | ||
|
|
bde1f6d199 | ||
|
|
0d7ee6f208 | ||
|
|
78adaecb89 | ||
|
|
f89d91783b | ||
|
|
a18e1a0161 | ||
|
|
4308b8a4a5 | ||
|
|
280d98bad9 | ||
|
|
ae5bf747c9 | ||
|
|
1ae0820ecc | ||
|
|
c09473f41e | ||
|
|
99a3e0c399 | ||
|
|
d2bd4a213b | ||
|
|
1dcb0b52e2 | ||
|
|
409c9c4e23 | ||
|
|
aa54e14c37 | ||
|
|
3ffe36e5ed | ||
|
|
3b2c74042e | ||
|
|
11ae938146 | ||
|
|
f11bb254a5 | ||
|
|
5187138547 | ||
|
|
2bb6387dae | ||
|
|
ca293dc0e7 | ||
|
|
ea1d4e7f50 | ||
|
|
0b681c471e | ||
|
|
80267aa418 | ||
|
|
36c31a21b9 | ||
|
|
51725d3d9c | ||
|
|
05d3354570 | ||
|
|
8799a15e73 | ||
|
|
2cde9a82a0 | ||
|
|
2c1fa628a2 | ||
|
|
a67fc64afb | ||
|
|
6bbdc2bae1 | ||
|
|
50d7fd776f | ||
|
|
1c38f705a7 | ||
|
|
b643939cc4 | ||
|
|
3ed3e93b25 | ||
|
|
f6ea09e581 | ||
|
|
998f89216e | ||
|
|
aefc6ff7b4 | ||
|
|
66615f1a96 | ||
|
|
a5dc91c175 | ||
|
|
d04436aa0a | ||
|
|
6542b8b198 | ||
|
|
6813787fc7 | ||
|
|
afdb24610d | ||
|
|
58e4bf1cc3 | ||
|
|
28761fc960 | ||
|
|
69e54ab410 | ||
|
|
116ceb6f93 | ||
|
|
5d022a575a | ||
|
|
e8fd0f3531 | ||
|
|
8103c399d5 | ||
|
|
cf3e7f90d6 | ||
|
|
22bfac746e | ||
|
|
066a3b8b52 | ||
|
|
48141c0693 | ||
|
|
576e21eb65 | ||
|
|
a51f5edbc8 | ||
|
|
be393a9d10 | ||
|
|
ef59f38ec4 | ||
|
|
47120fae01 | ||
|
|
93a4327921 | ||
|
|
0f2bbd7bfd | ||
|
|
c0417c1989 | ||
|
|
fb6cfa45fd | ||
|
|
b875cea10d | ||
|
|
516372e5db | ||
|
|
0899a1052e | ||
|
|
32bf17c076 | ||
|
|
66a6a8f33c | ||
|
|
007fe6a030 | ||
|
|
f26253ec49 | ||
|
|
f2dc287f14 | ||
|
|
3fe3151af7 | ||
|
|
27eefd8705 | ||
|
|
097e0f38ff | ||
|
|
ce26b566a4 | ||
|
|
0e14bc1e02 | ||
|
|
ce6796ed9b | ||
|
|
c90cecc2fb | ||
|
|
b6bbcb0609 | ||
|
|
23f6832d9c | ||
|
|
88dace75a1 | ||
|
|
8eb140fd65 | ||
|
|
1f09f3d096 | ||
|
|
66be85a41f | ||
|
|
814c11167e | ||
|
|
57ddd5086f | ||
|
|
c171547037 |
@@ -45,6 +45,10 @@ sure to include:
|
||||
* Any error messages generated
|
||||
* Screenshots (if applicable)
|
||||
|
||||
* Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title.
|
||||
The issue will be reviewed by a moderator after submission and the appropriate
|
||||
labels will be applied.
|
||||
|
||||
* Keep in mind that we prioritize bugs based on their severity and how
|
||||
much work is required to resolve them. It may take some time for someone
|
||||
to address your issue.
|
||||
@@ -91,6 +95,10 @@ following:
|
||||
* Any third-party libraries or other resources which would be
|
||||
involved
|
||||
|
||||
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title.
|
||||
The issue will be reviewed by a moderator after submission and the appropriate
|
||||
labels will be applied.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
* Be sure to open an issue before starting work on a pull request, and
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -1,20 +0,0 @@
|
||||
FROM python:2.7-wheezy
|
||||
|
||||
WORKDIR /opt/netbox
|
||||
|
||||
ARG BRANCH=master
|
||||
ARG URL=https://github.com/digitalocean/netbox.git
|
||||
RUN git clone --depth 1 $URL -b $BRANCH . && \
|
||||
apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev 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/"]
|
||||
11
README.md
11
README.md
@@ -1,7 +1,3 @@
|
||||
**The [2017 NetBox User Survey](https://goo.gl/forms/75HnNS2iE0Y1hVFH3) is open!** Please consider taking a moment to respond. Your feedback helps shape the pace and focus of NetBox development. The survey will remain open until 2017-03-31. Results will be published on the mailing list.
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.
|
||||
@@ -14,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) |
|
||||
@@ -33,5 +31,6 @@ 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))
|
||||
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)
|
||||
|
||||
@@ -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"';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ $ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
|
||||
}
|
||||
```
|
||||
|
||||
However, if the `[LOGIN_REQUIRED](../configuration/optional-settings/#login_required)` configuration setting has been set to `True`, all requests must be authenticated.
|
||||
However, if the [`LOGIN_REQUIRED`](../configuration/optional-settings/#login_required) configuration setting has been set to `True`, all requests must be authenticated.
|
||||
|
||||
```
|
||||
$ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/
|
||||
|
||||
@@ -120,7 +120,7 @@ Vary: Accept
|
||||
}
|
||||
```
|
||||
|
||||
The default page size derives from the `[PAGINATE_COUNT](../configuration/optional-settings/#paginate_count)` configuration setting, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
|
||||
The default page size derives from the [`PAGINATE_COUNT`](../configuration/optional-settings/#paginate_count) configuration setting, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
|
||||
|
||||
```
|
||||
http://localhost:8000/api/dcim/devices/?limit=100
|
||||
@@ -136,3 +136,8 @@ The response will return devices 1 through 100. The URL provided in the `next` a
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/optional-settings/#max_page_size) setting, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
|
||||
|
||||
!!! warning
|
||||
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
|
||||
|
||||
@@ -83,6 +83,34 @@ Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce uni
|
||||
|
||||
---
|
||||
|
||||
## LOGGING
|
||||
|
||||
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
|
||||
|
||||
The Django framework on which NetBox runs allows for the customization of logging, e.g. to write logs to file. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/1.11/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a file:
|
||||
|
||||
```
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'file': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': '/var/log/netbox.log',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LOGIN_REQUIRED
|
||||
|
||||
Default: False
|
||||
@@ -99,6 +127,14 @@ Setting this to True will display a "maintenance mode" banner at the top of ever
|
||||
|
||||
---
|
||||
|
||||
## MAX_PAGE_SIZE
|
||||
|
||||
Default: 1000
|
||||
|
||||
An API consumer can request an arbitrary number of objects by appending the "limit" parameter to the URL (e.g. `?limit=1000`). This setting defines the maximum limit. Setting it to `0` or `None` will allow an API consumer to request all objects by specifying `?limit=0`.
|
||||
|
||||
---
|
||||
|
||||
## NETBOX_USERNAME
|
||||
|
||||
## NETBOX_PASSWORD
|
||||
|
||||
@@ -10,10 +10,6 @@ Sites can be assigned an optional facility ID to identify the actual facility ho
|
||||
|
||||
Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned.
|
||||
|
||||
### Regions
|
||||
|
||||
Sites can optionally be arranged by geographic region. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy.
|
||||
|
||||
---
|
||||
|
||||
# Racks
|
||||
|
||||
@@ -119,7 +119,14 @@ Each line of the **device patterns** field represents a hierarchical layer withi
|
||||
```
|
||||
core-switch-[abcd]
|
||||
dist-switch\d
|
||||
access-switch\d+,oob-switch\d+
|
||||
access-switch\d+;oob-switch\d+
|
||||
```
|
||||
|
||||
Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.
|
||||
|
||||
# Image Attachments
|
||||
|
||||
Certain objects within NetBox (namely sites, racks, and devices) can have photos or other images attached to them. (Note that _only_ image files are supported.) Each attachment may optionally be assigned a name; if omitted, the attachment will be represented by its file name.
|
||||
|
||||
!!! note
|
||||
If you experience a server error while attempting to upload an image attachment, verify that the system user NetBox runs as has write permission to the media root directory (`netbox/media/`).
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,4 @@
|
||||
This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to
|
||||
built-in Django users in the event of a failure.
|
||||
This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to built-in Django users in the event of a failure.
|
||||
|
||||
# Requirements
|
||||
|
||||
@@ -29,6 +28,9 @@ Create a file in the same directory as `configuration.py` (typically `netbox/net
|
||||
|
||||
## General Server Configuration
|
||||
|
||||
!!! info
|
||||
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
|
||||
|
||||
```python
|
||||
import ldap
|
||||
|
||||
@@ -52,6 +54,9 @@ LDAP_IGNORE_CERT_ERRORS = True
|
||||
|
||||
## User Authentication
|
||||
|
||||
!!! info
|
||||
When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
|
||||
|
||||
```python
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
|
||||
@@ -99,3 +104,16 @@ AUTH_LDAP_FIND_GROUP_PERMS = True
|
||||
AUTH_LDAP_CACHE_GROUPS = True
|
||||
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
|
||||
```
|
||||
|
||||
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
|
||||
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
|
||||
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
|
||||
|
||||
It is also possible map user attributes to Django attributes:
|
||||
|
||||
```python
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
"first_name": "givenName",
|
||||
"last_name": "sn",
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
# Installation
|
||||
|
||||
**Debian/Ubuntu**
|
||||
**Ubuntu**
|
||||
|
||||
Python 3:
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
|
||||
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
|
||||
```
|
||||
|
||||
Python 2:
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
|
||||
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
**CentOS**
|
||||
|
||||
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:
|
||||
@@ -54,13 +56,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use
|
||||
|
||||
If `git` is not already installed, install it:
|
||||
|
||||
**Debian/Ubuntu**
|
||||
**Ubuntu**
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y git
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
**CentOS**
|
||||
|
||||
```no-highlight
|
||||
# yum install -y git
|
||||
@@ -83,6 +85,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
|
||||
```
|
||||
@@ -139,11 +149,14 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
|
||||
|
||||
# Run Database Migrations
|
||||
|
||||
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
||||
!!! warning
|
||||
The examples on the rest of this page call the `python` executable, which will be Python2 on most systems. Replace this with `python3` if you're running NetBox on Python3.
|
||||
|
||||
Before NetBox can run, we need to install the database schema. This is done by running `python manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
||||
|
||||
```no-highlight
|
||||
# cd /opt/netbox/netbox/
|
||||
# ./manage.py migrate
|
||||
# python manage.py migrate
|
||||
Operations to perform:
|
||||
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
|
||||
Running migrations:
|
||||
@@ -161,7 +174,7 @@ If this step results in a PostgreSQL authentication error, ensure that the usern
|
||||
NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox:
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py createsuperuser
|
||||
# python manage.py createsuperuser
|
||||
Username: admin
|
||||
Email address: admin@example.com
|
||||
Password:
|
||||
@@ -172,7 +185,7 @@ Superuser created successfully.
|
||||
# Collect Static Files
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py collectstatic
|
||||
# python manage.py collectstatic --no-input
|
||||
|
||||
You have requested to collect static files at the destination
|
||||
location as specified in your settings:
|
||||
@@ -193,7 +206,7 @@ NetBox ships with some initial data to help you get started: RIR definitions, co
|
||||
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py loaddata initial_data
|
||||
# python manage.py loaddata initial_data
|
||||
Installed 43 object(s) from 4 fixture(s)
|
||||
```
|
||||
|
||||
@@ -202,7 +215,7 @@ Installed 43 object(s) from 4 fixture(s)
|
||||
At this point, NetBox should be able to run. We can verify this by starting a development instance:
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py runserver 0.0.0.0:8000 --insecure
|
||||
# python manage.py runserver 0.0.0.0:8000 --insecure
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
NetBox requires a PostgreSQL database to store data. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).)
|
||||
|
||||
!!! note
|
||||
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 6.9. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||
|
||||
# Installation
|
||||
|
||||
**Debian/Ubuntu**
|
||||
**Ubuntu**
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y postgresql libpq-dev python-psycopg2
|
||||
# apt-get update
|
||||
# apt-get install -y postgresql libpq-dev
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
**CentOS**
|
||||
|
||||
```no-highlight
|
||||
# yum install -y postgresql postgresql-server postgresql-devel python-psycopg2
|
||||
# yum install -y postgresql postgresql-server postgresql-devel
|
||||
# postgresql-setup initdb
|
||||
```
|
||||
|
||||
|
||||
@@ -52,12 +52,27 @@ Once the new code is in place, run the upgrade script (which may need to be run
|
||||
# ./upgrade.sh
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below.
|
||||
|
||||
```no-highlight
|
||||
# ./upgrade.sh -2
|
||||
```
|
||||
|
||||
This script:
|
||||
|
||||
* Installs or upgrades any new required Python packages
|
||||
* Applies any database migrations that were included in the release
|
||||
* Collects all static files to be served by the HTTP service
|
||||
|
||||
!!! note
|
||||
It's possible that the upgrade script will display a notice warning of unreflected database migrations:
|
||||
|
||||
Your models have changes that are not yet reflected in a migration, and so won't be applied.
|
||||
Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.
|
||||
|
||||
This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are intentionally modifying the database schema.
|
||||
|
||||
# Restart the WSGI Service
|
||||
|
||||
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
|
||||
|
||||
!!! info
|
||||
Only Debian/Ubuntu instructions are provided here, but the installation process for CentOS/RHEL does not differ much. Please consult the documentation for those distributions for details.
|
||||
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y gunicorn supervisor
|
||||
@@ -25,7 +25,7 @@ server {
|
||||
|
||||
server_name netbox.example.com;
|
||||
|
||||
access_log off;
|
||||
client_max_body_size 25m;
|
||||
|
||||
location /static/ {
|
||||
alias /opt/netbox/netbox/static/;
|
||||
@@ -73,6 +73,9 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
|
||||
|
||||
Alias /static /opt/netbox/netbox/static
|
||||
|
||||
# Needed to allow token-based API authentication
|
||||
WSGIPassAuthorization on
|
||||
|
||||
<Directory /opt/netbox/netbox/static>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
AllowOverride None
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Provider, CircuitType, Circuit
|
||||
|
||||
|
||||
@admin.register(Provider)
|
||||
class ProviderAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'asn']
|
||||
|
||||
|
||||
@admin.register(CircuitType)
|
||||
class CircuitTypeAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
|
||||
|
||||
@admin.register(Circuit)
|
||||
class CircuitAdmin(admin.ModelAdmin):
|
||||
list_display = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate_human']
|
||||
list_filter = ['provider', 'type', 'tenant']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(CircuitAdmin, self).get_queryset(request)
|
||||
return qs.select_related('provider', 'type', 'tenant')
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
@@ -28,11 +30,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 +84,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',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
@@ -22,4 +24,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
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from circuits import filters
|
||||
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
|
||||
SlugField,
|
||||
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
|
||||
SmallTextarea, SlugField,
|
||||
)
|
||||
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
@@ -36,15 +39,18 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class ProviderFromCSVForm(forms.ModelForm):
|
||||
class ProviderCSVForm(forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||
|
||||
|
||||
class ProviderImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
|
||||
help_texts = {
|
||||
'name': 'Provider name',
|
||||
'asn': '32-bit autonomous system number',
|
||||
'portal_url': 'Portal URL',
|
||||
'comments': 'Free-form comments',
|
||||
}
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -83,12 +89,15 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
||||
'comments',
|
||||
]
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'install_date': "Format: YYYY-MM-DD",
|
||||
@@ -96,21 +105,36 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class CircuitFromCSVForm(forms.ModelForm):
|
||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Provider not found.'})
|
||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
class CircuitCSVForm(forms.ModelForm):
|
||||
provider = forms.ModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent provider',
|
||||
error_messages={
|
||||
'invalid_choice': 'Provider not found.'
|
||||
}
|
||||
)
|
||||
type = forms.ModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Type of circuit',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid circuit type.'
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.'
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
|
||||
|
||||
|
||||
class CircuitImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -152,15 +176,18 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack'}
|
||||
)
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
@@ -168,8 +195,12 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
('rack', 'rack'),
|
||||
),
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
@@ -178,29 +209,27 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
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(),
|
||||
interface = ChainedModelChoiceField(
|
||||
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
),
|
||||
chains=(
|
||||
('device', 'device'),
|
||||
),
|
||||
required=False,
|
||||
label='Interface',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
||||
disabled_indicator='is_connected'
|
||||
api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical',
|
||||
disabled_indicator='connection'
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info']
|
||||
fields = [
|
||||
'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': "Physical circuit speed",
|
||||
'xconnect_id': "ID of the local cross-connect",
|
||||
@@ -212,49 +241,22 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
if instance and instance.interface is not None:
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['rack'] = instance.interface.device.rack
|
||||
initial['device'] = instance.interface.device
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# If an interface has been assigned, initialize rack and device
|
||||
if self.instance.interface:
|
||||
self.initial['rack'] = self.instance.interface.device.rack
|
||||
self.initial['device'] = self.instance.interface.device
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound:
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Limit device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
|
||||
elif self.initial.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# Limit interface choices
|
||||
if self.is_bound and self.data.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.data['device']).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
# Mark connected interfaces as disabled
|
||||
self.fields['interface'].choices = []
|
||||
for iface in self.fields['interface'].queryset:
|
||||
self.fields['interface'].choices.append(
|
||||
(iface.id, {
|
||||
'label': iface.name,
|
||||
'disabled': iface.is_connected and iface.pk != self.initial.get('interface'),
|
||||
})
|
||||
)
|
||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||
elif self.initial.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.initial['device']).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
)
|
||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||
else:
|
||||
interfaces = []
|
||||
self.fields['interface'].choices = [
|
||||
(iface.id, {
|
||||
'label': iface.name,
|
||||
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
||||
}) for iface in interfaces
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
81
netbox/circuits/migrations/0009_unicode_literals.py
Normal file
81
netbox/circuits/migrations/0009_unicode_literals.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0008_circuittermination_interface_protect_on_delete'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='cid',
|
||||
field=models.CharField(max_length=50, verbose_name='Circuit ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='commit_rate',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='install_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Date installed'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='port_speed',
|
||||
field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='pp_info',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='term_side',
|
||||
field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='upstream_speed',
|
||||
field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='xconnect_id',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='account',
|
||||
field=models.CharField(blank=True, max_length=30, verbose_name='Account number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='admin_contact',
|
||||
field=models.TextField(blank=True, verbose_name='Admin contact'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='noc_contact',
|
||||
field=models.TextField(blank=True, verbose_name='NOC contact'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='portal_url',
|
||||
field=models.URLField(blank=True, verbose_name='Portal'),
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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
|
||||
@@ -50,6 +52,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -105,12 +109,14 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
unique_together = ['provider', 'cid']
|
||||
|
||||
def __str__(self):
|
||||
return u'{} {}'.format(self.provider, self.cid)
|
||||
return '{} {}'.format(self.provider, self.cid)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
@@ -150,10 +156,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)')
|
||||
|
||||
@@ -162,7 +172,7 @@ class CircuitTermination(models.Model):
|
||||
unique_together = ['circuit', 'term_side']
|
||||
|
||||
def __str__(self):
|
||||
return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||
return '{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||
|
||||
def get_peer_termination(self):
|
||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
|
||||
@@ -19,9 +20,7 @@ CIRCUITTYPE_ACTIONS = """
|
||||
|
||||
class ProviderTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
account = tables.Column(verbose_name='Account')
|
||||
name = tables.LinkColumn()
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -29,17 +28,25 @@ class ProviderTable(BaseTable):
|
||||
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Provider
|
||||
fields = ('name', 'asn', 'account')
|
||||
|
||||
|
||||
#
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
name = tables.LinkColumn()
|
||||
circuit_count = tables.Column(verbose_name='Circuits')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
@@ -52,16 +59,34 @@ class CircuitTypeTable(BaseTable):
|
||||
|
||||
class CircuitTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
|
||||
type = tables.Column(verbose_name='Type')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
a_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
|
||||
args=[Accessor('termination_a.site.slug')])
|
||||
z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
|
||||
args=[Accessor('termination_z.site.slug')])
|
||||
description = tables.Column(verbose_name='Description')
|
||||
cid = tables.LinkColumn(verbose_name='ID')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
a_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
|
||||
args=[Accessor('termination_a.site.slug')]
|
||||
)
|
||||
z_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
|
||||
args=[Accessor('termination_z.site.slug')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||
|
||||
|
||||
class CircuitSearchTable(SearchTable):
|
||||
cid = tables.LinkColumn(verbose_name='ID')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
a_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_a.site'), args=[Accessor('termination_a.site.slug')]
|
||||
)
|
||||
z_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_z.site'), args=[Accessor('termination_z.site.slug')]
|
||||
)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
|
||||
# Providers
|
||||
url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'),
|
||||
url(r'^providers/add/$', views.ProviderEditView.as_view(), name='provider_add'),
|
||||
url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'),
|
||||
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.provider, name='provider'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||
|
||||
# Circuit types
|
||||
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
url(r'^circuit-types/add/$', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
|
||||
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
|
||||
url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
|
||||
# Circuits
|
||||
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
|
||||
url(r'^circuits/add/$', views.CircuitEditView.as_view(), name='circuit_add'),
|
||||
url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'),
|
||||
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||
|
||||
# Circuit terminations
|
||||
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
||||
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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 django.views.generic import View
|
||||
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
|
||||
from . import filters, forms, tables
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
||||
|
||||
@@ -28,28 +30,37 @@ class ProviderListView(ObjectListView):
|
||||
template_name = 'circuits/provider_list.html'
|
||||
|
||||
|
||||
def provider(request, slug):
|
||||
class ProviderView(View):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider).select_related('type', 'tenant')\
|
||||
.prefetch_related('terminations__site')
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||
def get(self, request, slug):
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
'provider': provider,
|
||||
'circuits': circuits,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider).select_related(
|
||||
'type', 'tenant'
|
||||
).prefetch_related(
|
||||
'terminations__site'
|
||||
)
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
'provider': provider,
|
||||
'circuits': circuits,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
|
||||
|
||||
class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.add_provider'
|
||||
model = Provider
|
||||
form_class = forms.ProviderForm
|
||||
template_name = 'circuits/provider_edit.html'
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderEditView(ProviderCreateView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
|
||||
|
||||
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'circuits.delete_provider'
|
||||
model = Provider
|
||||
@@ -58,9 +69,8 @@ class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_provider'
|
||||
form = forms.ProviderImportForm
|
||||
model_form = forms.ProviderCSVForm
|
||||
table = tables.ProviderTable
|
||||
template_name = 'circuits/provider_import.html'
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
@@ -90,15 +100,19 @@ class CircuitTypeListView(ObjectListView):
|
||||
template_name = 'circuits/circuittype_list.html'
|
||||
|
||||
|
||||
class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuittype'
|
||||
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.add_circuittype'
|
||||
model = CircuitType
|
||||
form_class = forms.CircuitTypeForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('circuits:circuittype_list')
|
||||
|
||||
|
||||
class CircuitTypeEditView(CircuitTypeCreateView):
|
||||
permission_required = 'circuits.change_circuittype'
|
||||
|
||||
|
||||
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
cls = CircuitType
|
||||
@@ -117,36 +131,41 @@ class CircuitListView(ObjectListView):
|
||||
template_name = 'circuits/circuit_list.html'
|
||||
|
||||
|
||||
def circuit(request, pk):
|
||||
class CircuitView(View):
|
||||
|
||||
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
|
||||
termination_a = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_A
|
||||
).first()
|
||||
termination_z = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_Z
|
||||
).first()
|
||||
def get(self, request, pk):
|
||||
|
||||
return render(request, 'circuits/circuit.html', {
|
||||
'circuit': circuit,
|
||||
'termination_a': termination_a,
|
||||
'termination_z': termination_z,
|
||||
})
|
||||
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
|
||||
termination_a = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_A
|
||||
).first()
|
||||
termination_z = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_Z
|
||||
).first()
|
||||
|
||||
return render(request, 'circuits/circuit.html', {
|
||||
'circuit': circuit,
|
||||
'termination_a': termination_a,
|
||||
'termination_z': termination_z,
|
||||
})
|
||||
|
||||
|
||||
class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.add_circuit'
|
||||
model = Circuit
|
||||
form_class = forms.CircuitForm
|
||||
fields_initial = ['provider']
|
||||
template_name = 'circuits/circuit_edit.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitEditView(CircuitCreateView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
|
||||
|
||||
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'circuits.delete_circuit'
|
||||
model = Circuit
|
||||
@@ -155,9 +174,8 @@ class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_circuit'
|
||||
form = forms.CircuitImportForm
|
||||
model_form = forms.CircuitCSVForm
|
||||
table = tables.CircuitTable
|
||||
template_name = 'circuits/circuit_import.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
@@ -226,11 +244,10 @@ def circuit_terminations_swap(request, pk):
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuittermination'
|
||||
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.add_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,10 +255,14 @@ 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()
|
||||
|
||||
|
||||
class CircuitTerminationEditView(CircuitTerminationCreateView):
|
||||
permission_required = 'circuits.change_circuittermination'
|
||||
|
||||
|
||||
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'circuits.delete_circuittermination'
|
||||
model = CircuitTermination
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from django.db.models import Count
|
||||
|
||||
from mptt.admin import MPTTModelAdmin
|
||||
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, InventoryItem, Platform,
|
||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region,
|
||||
Site,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Region)
|
||||
class RegionAdmin(MPTTModelAdmin):
|
||||
list_display = ['name', 'parent', 'slug']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Site)
|
||||
class SiteAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'facility', 'asn']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(RackGroup)
|
||||
class RackGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'site']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(RackRole)
|
||||
class RackRoleAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'color']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Rack)
|
||||
class RackAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
|
||||
|
||||
|
||||
@admin.register(RackReservation)
|
||||
class RackRackReservationAdmin(admin.ModelAdmin):
|
||||
list_display = ['rack', 'units', 'description', 'user', 'created']
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
||||
@admin.register(Manufacturer)
|
||||
class ManufacturerAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
|
||||
|
||||
class ConsolePortTemplateAdmin(admin.TabularInline):
|
||||
model = ConsolePortTemplate
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateAdmin(admin.TabularInline):
|
||||
model = ConsoleServerPortTemplate
|
||||
|
||||
|
||||
class PowerPortTemplateAdmin(admin.TabularInline):
|
||||
model = PowerPortTemplate
|
||||
|
||||
|
||||
class PowerOutletTemplateAdmin(admin.TabularInline):
|
||||
model = PowerOutletTemplate
|
||||
|
||||
|
||||
class InterfaceTemplateAdmin(admin.TabularInline):
|
||||
model = InterfaceTemplate
|
||||
|
||||
|
||||
class DeviceBayTemplateAdmin(admin.TabularInline):
|
||||
model = DeviceBayTemplate
|
||||
|
||||
|
||||
@admin.register(DeviceType)
|
||||
class DeviceTypeAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['model'],
|
||||
}
|
||||
inlines = [
|
||||
ConsolePortTemplateAdmin,
|
||||
ConsoleServerPortTemplateAdmin,
|
||||
PowerPortTemplateAdmin,
|
||||
PowerOutletTemplateAdmin,
|
||||
InterfaceTemplateAdmin,
|
||||
DeviceBayTemplateAdmin,
|
||||
]
|
||||
list_display = ['model', 'manufacturer', 'slug', 'part_number', 'u_height', 'console_ports', 'console_server_ports',
|
||||
'power_ports', 'power_outlets', 'interfaces', 'device_bays']
|
||||
list_filter = ['manufacturer']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return DeviceType.objects.annotate(
|
||||
console_port_count=Count('console_port_templates', distinct=True),
|
||||
cs_port_count=Count('cs_port_templates', distinct=True),
|
||||
power_port_count=Count('power_port_templates', distinct=True),
|
||||
power_outlet_count=Count('power_outlet_templates', distinct=True),
|
||||
interface_count=Count('interface_templates', distinct=True),
|
||||
devicebay_count=Count('device_bay_templates', distinct=True),
|
||||
)
|
||||
|
||||
def console_ports(self, instance):
|
||||
return instance.console_port_count
|
||||
|
||||
def console_server_ports(self, instance):
|
||||
return instance.cs_port_count
|
||||
|
||||
def power_ports(self, instance):
|
||||
return instance.power_port_count
|
||||
|
||||
def power_outlets(self, instance):
|
||||
return instance.power_outlet_count
|
||||
|
||||
def interfaces(self, instance):
|
||||
return instance.interface_count
|
||||
|
||||
def device_bays(self, instance):
|
||||
return instance.devicebay_count
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
@admin.register(DeviceRole)
|
||||
class DeviceRoleAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'color']
|
||||
|
||||
|
||||
@admin.register(Platform)
|
||||
class PlatformAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'rpc_client']
|
||||
|
||||
|
||||
class ConsolePortAdmin(admin.TabularInline):
|
||||
model = ConsolePort
|
||||
readonly_fields = ['cs_port']
|
||||
|
||||
|
||||
class ConsoleServerPortAdmin(admin.TabularInline):
|
||||
model = ConsoleServerPort
|
||||
|
||||
|
||||
class PowerPortAdmin(admin.TabularInline):
|
||||
model = PowerPort
|
||||
readonly_fields = ['power_outlet']
|
||||
|
||||
|
||||
class PowerOutletAdmin(admin.TabularInline):
|
||||
model = PowerOutlet
|
||||
|
||||
|
||||
class InterfaceAdmin(admin.TabularInline):
|
||||
model = Interface
|
||||
|
||||
|
||||
class DeviceBayAdmin(admin.TabularInline):
|
||||
model = DeviceBay
|
||||
fk_name = 'device'
|
||||
readonly_fields = ['installed_device']
|
||||
|
||||
|
||||
class InventoryItemAdmin(admin.TabularInline):
|
||||
model = InventoryItem
|
||||
readonly_fields = ['parent', 'discovered']
|
||||
|
||||
|
||||
@admin.register(Device)
|
||||
class DeviceAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
ConsolePortAdmin,
|
||||
ConsoleServerPortAdmin,
|
||||
PowerPortAdmin,
|
||||
PowerOutletAdmin,
|
||||
InterfaceAdmin,
|
||||
DeviceBayAdmin,
|
||||
InventoryItemAdmin,
|
||||
]
|
||||
list_display = ['display_name', 'device_type_full_name', 'device_role', 'primary_ip', 'rack', 'position', 'asset_tag',
|
||||
'serial']
|
||||
list_filter = ['device_role']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(DeviceAdmin, self).get_queryset(request)
|
||||
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')
|
||||
|
||||
def device_type_full_name(self, obj):
|
||||
return obj.device_type.full_name
|
||||
device_type_full_name.short_description = 'Device type'
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
@@ -66,13 +68,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 +152,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 +265,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 +478,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 +492,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)
|
||||
|
||||
@@ -581,9 +583,18 @@ class WritablePowerPortSerializer(serializers.ModelSerializer):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class NestedInterfaceSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class InterfaceSerializer(serializers.ModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
||||
lag = NestedInterfaceSerializer()
|
||||
connection = serializers.SerializerMethodField(read_only=True)
|
||||
connected_interface = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
@@ -608,10 +619,12 @@ class InterfaceSerializer(serializers.ModelSerializer):
|
||||
class PeerInterfaceSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
||||
lag = NestedInterfaceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
||||
fields = ['id', 'url', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class WritableInterfaceSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
@@ -50,10 +52,13 @@ router.register(r'interfaces', views.InterfaceViewSet)
|
||||
router.register(r'device-bays', views.DeviceBayViewSet)
|
||||
router.register(r'inventory-items', views.InventoryItemViewSet)
|
||||
|
||||
# Interface connections
|
||||
# Connections
|
||||
router.register(r'console-connections', views.ConsoleConnectionViewSet, base_name='consoleconnections')
|
||||
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
|
||||
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
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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 ModelViewSet, 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
|
||||
@@ -38,8 +41,8 @@ class RegionViewSet(WritableSerializerMixin, ModelViewSet):
|
||||
class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
queryset = Site.objects.select_related('region', 'tenant')
|
||||
serializer_class = serializers.SiteSerializer
|
||||
filter_class = filters.SiteFilter
|
||||
write_serializer_class = serializers.WritableSiteSerializer
|
||||
filter_class = filters.SiteFilter
|
||||
|
||||
@detail_route()
|
||||
def graphs(self, request, pk=None):
|
||||
@@ -59,8 +62,8 @@ class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
class RackGroupViewSet(WritableSerializerMixin, ModelViewSet):
|
||||
queryset = RackGroup.objects.select_related('site')
|
||||
serializer_class = serializers.RackGroupSerializer
|
||||
filter_class = filters.RackGroupFilter
|
||||
write_serializer_class = serializers.WritableRackGroupSerializer
|
||||
filter_class = filters.RackGroupFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -135,6 +138,7 @@ class DeviceTypeViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
serializer_class = serializers.DeviceTypeSerializer
|
||||
write_serializer_class = serializers.WritableDeviceTypeSerializer
|
||||
filter_class = filters.DeviceTypeFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -302,13 +306,26 @@ class InventoryItemViewSet(WritableSerializerMixin, ModelViewSet):
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
# Connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = ConsolePort.objects.select_related('device', 'cs_port__device').filter(cs_port__isnull=False)
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filter_class = filters.ConsoleConnectionFilter
|
||||
|
||||
|
||||
class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = PowerPort.objects.select_related('device', 'power_outlet__device').filter(power_outlet__isnull=False)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filter_class = filters.PowerConnectionFilter
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import EUI, mac_unix_expanded
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
@@ -8,9 +10,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, InterfaceConnection, 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 +150,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 +187,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')
|
||||
@@ -240,7 +279,7 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['name']
|
||||
fields = ['name', 'form_factor']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
@@ -336,10 +375,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 +391,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 +439,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)',
|
||||
@@ -441,10 +479,19 @@ class InterfaceFilter(DeviceComponentFilterSet):
|
||||
method='filter_type',
|
||||
label='Interface type',
|
||||
)
|
||||
lag_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='lag',
|
||||
queryset=Interface.objects.all(),
|
||||
label='LAG interface (ID)',
|
||||
)
|
||||
mac_address = django_filters.CharFilter(
|
||||
method='_mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name']
|
||||
fields = ['name', 'form_factor']
|
||||
|
||||
def filter_type(self, queryset, name, value):
|
||||
value = value.strip().lower()
|
||||
@@ -456,6 +503,15 @@ class InterfaceFilter(DeviceComponentFilterSet):
|
||||
return queryset.filter(form_factor=IFACE_FF_LAG)
|
||||
return queryset
|
||||
|
||||
def _mac_address(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
return queryset.filter(mac_address=value)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class DeviceBayFilter(DeviceComponentFilterSet):
|
||||
|
||||
@@ -476,42 +532,70 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = []
|
||||
model = ConsolePort
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(cs_port__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(cs_port__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class PowerConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = []
|
||||
model = PowerPort
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(power_outlet__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(power_outlet__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = []
|
||||
fields = ['connection_status']
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -520,3 +604,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
Q(interface_a__device__site__slug=value) |
|
||||
Q(interface_b__device__site__slug=value)
|
||||
)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__name__icontains=value) |
|
||||
Q(interface_b__device__name__icontains=value)
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import EUI, AddrFormatError
|
||||
|
||||
from django import forms
|
||||
|
||||
1273
netbox/dcim/forms.py
1273
netbox/dcim/forms.py
File diff suppressed because it is too large
Load Diff
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),
|
||||
),
|
||||
]
|
||||
209
netbox/dcim/migrations/0037_unicode_literals.py
Normal file
209
netbox/dcim/migrations/0037_unicode_literals.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0036_add_ff_juniper_vcp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='cs_port',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='asset_tag',
|
||||
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='face',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='position',
|
||||
field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip4',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='primary_ip6',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebay',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='interface_ordering',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_console_server',
|
||||
field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_full_depth',
|
||||
field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_network_device',
|
||||
field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='is_pdu',
|
||||
field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='part_number',
|
||||
field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='lag',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mac_address',
|
||||
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mgmt_only',
|
||||
field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfaceconnection',
|
||||
name='connection_status',
|
||||
field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='mgmt_only',
|
||||
field=models.BooleanField(default=False, verbose_name='Management only'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='discovered',
|
||||
field=models.BooleanField(default=False, verbose_name='Discovered'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='part_id',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='serial',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='rpc_client',
|
||||
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='RPC client'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='desc_units',
|
||||
field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='width',
|
||||
field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='asn',
|
||||
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='contact_email',
|
||||
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'),
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
from collections import OrderedDict
|
||||
from itertools import count, groupby
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
@@ -8,21 +10,20 @@ 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
|
||||
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
|
||||
from extras.models import CustomFieldModel, CustomField, CustomFieldValue, ImageAttachment
|
||||
from extras.rpc import RPC_CLIENTS
|
||||
from tenancy.models import Tenant
|
||||
from utilities.fields import ColorField, NullableCharField
|
||||
from utilities.managers import NaturalOrderByManager
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
from utilities.utils import csv_format
|
||||
|
||||
from .fields import ASNField, MACAddressField
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -254,9 +276,14 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
contact_email = models.EmailField(blank=True, verbose_name="Contact E-mail")
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
images = GenericRelation(ImageAttachment)
|
||||
|
||||
objects = SiteManager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -313,7 +340,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']
|
||||
@@ -323,7 +350,7 @@ class RackGroup(models.Model):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return u'{} - {}'.format(self.site.name, self.name)
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||
@@ -375,9 +402,14 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
help_text='Units are numbered top-to-bottom')
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
images = GenericRelation(ImageAttachment)
|
||||
|
||||
objects = RackManager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
@@ -386,7 +418,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name
|
||||
return self.display_name or super(Rack, self).__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
@@ -442,8 +474,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
@property
|
||||
def display_name(self):
|
||||
if self.facility_id:
|
||||
return u"{} ({})".format(self.name, self.facility_id)
|
||||
return self.name
|
||||
return "{} ({})".format(self.name, self.facility_id)
|
||||
elif self.name:
|
||||
return self.name
|
||||
return ""
|
||||
|
||||
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
|
||||
"""
|
||||
@@ -543,7 +577,7 @@ class RackReservation(models.Model):
|
||||
ordering = ['created']
|
||||
|
||||
def __str__(self):
|
||||
return u"Reservation for rack {}".format(self.rack)
|
||||
return "Reservation for rack {}".format(self.rack)
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -553,7 +587,7 @@ class RackReservation(models.Model):
|
||||
invalid_units = [u for u in self.units if u not in self.rack.units]
|
||||
if invalid_units:
|
||||
raise ValidationError({
|
||||
'units': u"Invalid unit(s) for {}U rack: {}".format(
|
||||
'units': "Invalid unit(s) for {}U rack: {}".format(
|
||||
self.rack.u_height,
|
||||
', '.join([str(u) for u in invalid_units]),
|
||||
),
|
||||
@@ -571,6 +605,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
|
||||
@@ -698,7 +741,7 @@ class DeviceType(models.Model, CustomFieldModel):
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return u'{} {}'.format(self.manufacturer.name, self.model)
|
||||
return '{} {}'.format(self.manufacturer.name, self.model)
|
||||
|
||||
@property
|
||||
def is_parent_device(self):
|
||||
@@ -777,13 +820,13 @@ class InterfaceManager(models.Manager):
|
||||
|
||||
def order_naturally(self, method=IFACE_ORDERING_POSITION):
|
||||
"""
|
||||
Naturally order interfaces by their name and numeric position. The sort method must be one of the defined
|
||||
Naturally order interfaces by their type and numeric position. The sort method must be one of the defined
|
||||
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:
|
||||
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
|
||||
slot, subslot, position, channel, and virtual circuit:
|
||||
|
||||
{name}{slot}/{subslot}/{position}:{channel}
|
||||
{type}{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:
|
||||
@@ -793,21 +836,24 @@ 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.
|
||||
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
|
||||
the prescribed fields.
|
||||
"""
|
||||
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', '_type', 'name'),
|
||||
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
|
||||
}[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),
|
||||
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".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)
|
||||
|
||||
|
||||
@@ -904,11 +950,11 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
|
||||
|
||||
Each Device must be assigned to a Rack, although associating it with a particular rack face or unit is optional (for
|
||||
example, vertically mounted PDUs do not consume rack units).
|
||||
Each Device must be assigned to a site, and optionally to a rack within that site. Associating a device with a
|
||||
particular rack face or unit is optional (for example, vertically mounted PDUs do not consume rack units).
|
||||
|
||||
When a new Device is created, console/power/interface components are created along with it as dictated by the
|
||||
component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
|
||||
When a new Device is created, console/power/interface/device bay components are created along with it as dictated
|
||||
by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
|
||||
creation of a Device.
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
|
||||
@@ -917,30 +963,43 @@ 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)
|
||||
|
||||
objects = DeviceManager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = ['rack', 'position', 'face']
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name
|
||||
return self.display_name or super(Device, self).__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
@@ -1048,7 +1107,9 @@ 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.group.name if self.rack and self.rack.group else None,
|
||||
self.rack.name if self.rack else None,
|
||||
self.position,
|
||||
self.get_face_display(),
|
||||
@@ -1058,12 +1119,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
def display_name(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
elif self.position:
|
||||
return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position)
|
||||
elif self.rack:
|
||||
return u"{} ({})".format(self.device_type, self.rack.name)
|
||||
else:
|
||||
return u"{} ({})".format(self.device_type, self.site.name)
|
||||
elif hasattr(self, 'device_type'):
|
||||
return "{}".format(self.device_type)
|
||||
return ""
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
@@ -1091,6 +1149,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.
|
||||
@@ -1115,6 +1176,8 @@ class ConsolePort(models.Model):
|
||||
verbose_name='Console server port', blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
csv_headers = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
@@ -1184,6 +1247,8 @@ class PowerPort(models.Model):
|
||||
blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
@@ -1273,7 +1338,7 @@ class Interface(models.Model):
|
||||
# An interface's LAG must belong to the same device
|
||||
if self.lag and self.lag.device != self.device:
|
||||
raise ValidationError({
|
||||
'lag': u"The selected LAG interface ({}) belongs to a different device ({}).".format(
|
||||
'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format(
|
||||
self.lag.name, self.lag.device.name
|
||||
)
|
||||
})
|
||||
@@ -1281,14 +1346,14 @@ class Interface(models.Model):
|
||||
# A virtual interface cannot have a parent LAG
|
||||
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
|
||||
raise ValidationError({
|
||||
'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
||||
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
||||
})
|
||||
|
||||
# Only a LAG can have LAG members
|
||||
if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists():
|
||||
raise ValidationError({
|
||||
'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format(
|
||||
u", ".join([iface.name for iface in self.member_interfaces.all()])
|
||||
", ".join([iface.name for iface in self.member_interfaces.all()])
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1345,11 +1410,16 @@ class InterfaceConnection(models.Model):
|
||||
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED,
|
||||
verbose_name='Status')
|
||||
|
||||
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||
|
||||
def clean(self):
|
||||
if self.interface_a == self.interface_b:
|
||||
raise ValidationError({
|
||||
'interface_b': "Cannot connect an interface to itself."
|
||||
})
|
||||
try:
|
||||
if self.interface_a == self.interface_b:
|
||||
raise ValidationError({
|
||||
'interface_b': "Cannot connect an interface to itself."
|
||||
})
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
@@ -1381,7 +1451,7 @@ class DeviceBay(models.Model):
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return u'{} - {}'.format(self.device.name, self.name)
|
||||
return '{} - {}'.format(self.device.name, self.name)
|
||||
|
||||
def clean(self):
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
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 +65,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 +93,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 = """
|
||||
@@ -100,6 +103,10 @@ DEVICE_PRIMARY_IP = """
|
||||
{{ record.primary_ip4.address.ip|default:"" }}
|
||||
"""
|
||||
|
||||
SUBDEVICE_ROLE_TEMPLATE = """
|
||||
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph value %}
|
||||
@@ -132,11 +139,9 @@ class RegionTable(BaseTable):
|
||||
|
||||
class SiteTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
|
||||
facility = tables.Column(verbose_name='Facility')
|
||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK, verbose_name='Region')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
name = tables.LinkColumn()
|
||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
||||
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
||||
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
||||
@@ -151,6 +156,16 @@ class SiteTable(BaseTable):
|
||||
)
|
||||
|
||||
|
||||
class SiteSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Site
|
||||
fields = ('name', 'facility', 'region', 'tenant', 'asn')
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
@@ -193,20 +208,33 @@ class RackRoleTable(BaseTable):
|
||||
|
||||
class RackTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
name = tables.LinkColumn()
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
facility_id = tables.Column(verbose_name='Facility ID')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
role = tables.TemplateColumn(RACK_ROLE, verbose_name='Role')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
role = tables.TemplateColumn(RACK_ROLE)
|
||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
|
||||
devices = tables.Column(accessor=Accessor('device_count'))
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices',
|
||||
'get_utilization')
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
|
||||
)
|
||||
|
||||
|
||||
class RackSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
role = tables.TemplateColumn(RACK_ROLE)
|
||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Rack
|
||||
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height')
|
||||
|
||||
|
||||
class RackImportTable(BaseTable):
|
||||
@@ -219,7 +247,24 @@ class RackImportTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
||||
fields = ('name', 'site', 'group', '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')
|
||||
|
||||
|
||||
#
|
||||
@@ -245,15 +290,36 @@ class ManufacturerTable(BaseTable):
|
||||
|
||||
class DeviceTypeTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
manufacturer = tables.Column(verbose_name='Manufacturer')
|
||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||
part_number = tables.Column(verbose_name='Part Number')
|
||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
||||
is_console_server = tables.BooleanColumn(verbose_name='CS')
|
||||
is_pdu = tables.BooleanColumn(verbose_name='PDU')
|
||||
is_network_device = tables.BooleanColumn(verbose_name='Net')
|
||||
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
|
||||
instance_count = tables.Column(verbose_name='Instances')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count')
|
||||
fields = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role', 'instance_count'
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeSearchTable(SearchTable):
|
||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
||||
is_console_server = tables.BooleanColumn(verbose_name='CS')
|
||||
is_pdu = tables.BooleanColumn(verbose_name='PDU')
|
||||
is_network_device = tables.BooleanColumn(verbose_name='Net')
|
||||
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = DeviceType
|
||||
fields = (
|
||||
'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -362,24 +428,45 @@ class PlatformTable(BaseTable):
|
||||
|
||||
class DeviceTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
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')
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
||||
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')])
|
||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||
device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name)
|
||||
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
|
||||
template_code=DEVICE_PRIMARY_IP)
|
||||
device_type = tables.LinkColumn(
|
||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name
|
||||
)
|
||||
primary_ip = tables.TemplateColumn(
|
||||
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
|
||||
|
||||
class DeviceSearchTable(SearchTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
||||
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')])
|
||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||
device_type = tables.LinkColumn(
|
||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name
|
||||
)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Device
|
||||
fields = ('name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
|
||||
|
||||
|
||||
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')
|
||||
@@ -389,7 +476,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
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
@@ -1933,6 +1935,92 @@ class InventoryItemTest(HttpStatusMixin, APITestCase):
|
||||
self.assertEqual(InventoryItem.objects.count(), 2)
|
||||
|
||||
|
||||
class ConsoleConnectionTest(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)}
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
||||
)
|
||||
devicerole = DeviceRole.objects.create(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
)
|
||||
device1 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
|
||||
)
|
||||
device2 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 2', site=site
|
||||
)
|
||||
cs_port1 = ConsoleServerPort.objects.create(device=device1, name='Test CS Port 1')
|
||||
cs_port2 = ConsoleServerPort.objects.create(device=device1, name='Test CS Port 2')
|
||||
cs_port3 = ConsoleServerPort.objects.create(device=device1, name='Test CS Port 3')
|
||||
ConsolePort.objects.create(
|
||||
device=device2, cs_port=cs_port1, name='Test Console Port 1', connection_status=True
|
||||
)
|
||||
ConsolePort.objects.create(
|
||||
device=device2, cs_port=cs_port2, name='Test Console Port 2', connection_status=True
|
||||
)
|
||||
ConsolePort.objects.create(
|
||||
device=device2, cs_port=cs_port3, name='Test Console Port 3', connection_status=True
|
||||
)
|
||||
|
||||
def test_list_consoleconnections(self):
|
||||
|
||||
url = reverse('dcim-api:consoleconnections-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
|
||||
class PowerConnectionTest(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)}
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
||||
)
|
||||
devicerole = DeviceRole.objects.create(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
)
|
||||
device1 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
|
||||
)
|
||||
device2 = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 2', site=site
|
||||
)
|
||||
power_outlet1 = PowerOutlet.objects.create(device=device1, name='Test Power Outlet 1')
|
||||
power_outlet2 = PowerOutlet.objects.create(device=device1, name='Test Power Outlet 2')
|
||||
power_outlet3 = PowerOutlet.objects.create(device=device1, name='Test Power Outlet 3')
|
||||
PowerPort.objects.create(
|
||||
device=device2, power_outlet=power_outlet1, name='Test Power Port 1', connection_status=True
|
||||
)
|
||||
PowerPort.objects.create(
|
||||
device=device2, power_outlet=power_outlet2, name='Test Power Port 2', connection_status=True
|
||||
)
|
||||
PowerPort.objects.create(
|
||||
device=device2, power_outlet=power_outlet3, name='Test Power Port 3', connection_status=True
|
||||
)
|
||||
|
||||
def test_list_powerconnections(self):
|
||||
|
||||
url = reverse('dcim-api:powerconnections-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
|
||||
class InterfaceConnectionTest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import *
|
||||
|
||||
|
||||
|
||||
@@ -1,126 +1,135 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from ipam.views import ServiceEditView
|
||||
from extras.views import ImageAttachmentEditView
|
||||
from ipam.views import ServiceCreateView
|
||||
from secrets.views import secret_add
|
||||
|
||||
from .models import Device, Rack, Site
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'dcim'
|
||||
urlpatterns = [
|
||||
|
||||
# Regions
|
||||
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
|
||||
url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'),
|
||||
url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'),
|
||||
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
|
||||
|
||||
# Sites
|
||||
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
||||
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
|
||||
url(r'^sites/add/$', views.SiteCreateView.as_view(), name='site_add'),
|
||||
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
||||
|
||||
# Rack groups
|
||||
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
url(r'^rack-groups/add/$', views.RackGroupEditView.as_view(), name='rackgroup_add'),
|
||||
url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
|
||||
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
|
||||
# Rack roles
|
||||
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||
url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'),
|
||||
url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'),
|
||||
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
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'),
|
||||
url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
|
||||
url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
|
||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
|
||||
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
|
||||
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
|
||||
# Manufacturers
|
||||
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
url(r'^manufacturers/add/$', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
|
||||
url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
|
||||
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
|
||||
# Device types
|
||||
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'),
|
||||
url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||
url(r'^device-types/(?P<pk>\d+)/$', views.devicetype, name='devicetype'),
|
||||
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||
|
||||
# Console port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), name='devicetype_add_consoleport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
|
||||
|
||||
# Console server port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), name='devicetype_add_consoleserverport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
|
||||
|
||||
# Power port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), name='devicetype_add_powerport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
|
||||
|
||||
# Power outlet templates
|
||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), name='devicetype_add_poweroutlet'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
|
||||
|
||||
# Interface templates
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||
|
||||
# Device bay templates
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), name='devicetype_add_devicebay'),
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
|
||||
|
||||
# Device roles
|
||||
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
url(r'^device-roles/add/$', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
|
||||
url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
|
||||
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
|
||||
# Platforms
|
||||
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
|
||||
url(r'^platforms/add/$', views.PlatformEditView.as_view(), name='platform_add'),
|
||||
url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'),
|
||||
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
|
||||
# Devices
|
||||
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
||||
url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'),
|
||||
url(r'^devices/add/$', views.DeviceCreateView.as_view(), name='device_add'),
|
||||
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
||||
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
|
||||
url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
|
||||
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
||||
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+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
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<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'),
|
||||
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
|
||||
# Console ports
|
||||
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortAddView.as_view(), name='consoleport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
||||
@@ -129,7 +138,8 @@ urlpatterns = [
|
||||
|
||||
# Console server ports
|
||||
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortAddView.as_view(), name='consoleserverport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
||||
@@ -138,7 +148,7 @@ urlpatterns = [
|
||||
|
||||
# Power ports
|
||||
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortAddView.as_view(), name='powerport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
||||
@@ -147,7 +157,8 @@ urlpatterns = [
|
||||
|
||||
# Power outlets
|
||||
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletAddView.as_view(), name='poweroutlet_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
||||
@@ -156,8 +167,9 @@ urlpatterns = [
|
||||
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceAddView.as_view(), name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||
@@ -166,7 +178,7 @@ urlpatterns = [
|
||||
|
||||
# Device bays
|
||||
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayAddView.as_view(), name='devicebay_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
@@ -1,48 +1,154 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from __future__ import unicode_literals
|
||||
from datetime import datetime
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
|
||||
from extras.models import (
|
||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Custom fields
|
||||
#
|
||||
|
||||
class CustomFieldSerializer(serializers.BaseSerializer):
|
||||
"""
|
||||
Extends ModelSerializer to render any CustomFields and their values associated with an object.
|
||||
"""
|
||||
class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
|
||||
def to_representation(self, manager):
|
||||
def to_representation(self, obj):
|
||||
return obj
|
||||
|
||||
# Initialize custom fields dictionary
|
||||
data = {f.name: None for f in self.parent._custom_fields}
|
||||
def to_internal_value(self, data):
|
||||
|
||||
# Assign CustomFieldValues from database
|
||||
for cfv in manager.all():
|
||||
if cfv.field.type == CF_TYPE_SELECT:
|
||||
data[cfv.field.name] = CustomFieldChoiceSerializer(cfv.value).data
|
||||
else:
|
||||
data[cfv.field.name] = cfv.value
|
||||
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():
|
||||
|
||||
cf = custom_fields[field_name]
|
||||
|
||||
# Validate custom field name
|
||||
if field_name not in custom_fields:
|
||||
raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name))
|
||||
|
||||
# Validate boolean
|
||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError("Invalid value for boolean field {}: {}".format(field_name, value))
|
||||
|
||||
# Validate date
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValidationError("Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(
|
||||
field_name, value
|
||||
))
|
||||
|
||||
# Validate selected choice
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise ValidationError("{}: Choice selections must be passed as integers.".format(field_name))
|
||||
valid_choices = [c.pk for c in cf.choices.all()]
|
||||
if value not in valid_choices:
|
||||
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))
|
||||
|
||||
# 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("Missing required fields: {}".format(u", ".join(missing_fields)))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class CustomFieldModelSerializer(serializers.ModelSerializer):
|
||||
custom_fields = CustomFieldSerializer(source='custom_field_values')
|
||||
"""
|
||||
Extends ModelSerializer to render any CustomFields and their values associated with an object.
|
||||
"""
|
||||
custom_fields = CustomFieldsSerializer(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
def _populate_custom_fields(instance, fields):
|
||||
custom_fields = {f.name: None for f in fields}
|
||||
for cfv in instance.custom_field_values.all():
|
||||
if cfv.field.type == CF_TYPE_SELECT:
|
||||
custom_fields[cfv.field.name] = CustomFieldChoiceSerializer(cfv.value).data
|
||||
else:
|
||||
custom_fields[cfv.field.name] = cfv.value
|
||||
instance.custom_fields = custom_fields
|
||||
|
||||
super(CustomFieldModelSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
# Cache the list of custom fields for this model
|
||||
if self.instance is not None:
|
||||
|
||||
# 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)
|
||||
self._custom_fields = CustomField.objects.filter(obj_type=content_type)
|
||||
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': custom_field.serialize_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):
|
||||
"""
|
||||
Imitate utilities.api.ChoiceFieldSerializer
|
||||
"""
|
||||
value = serializers.IntegerField(source='pk')
|
||||
label = serializers.CharField(source='value')
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoice
|
||||
fields = ['id', 'value']
|
||||
fields = ['value', 'label']
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.serializers import NestedSiteSerializer
|
||||
from extras.models import ACTION_CHOICES, Graph, GRAPH_TYPE_CHOICES, ExportTemplate, TopologyMap, UserAction
|
||||
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
||||
from dcim.models import Device, Rack, Site
|
||||
from extras.models import (
|
||||
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
|
||||
)
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from utilities.api import ChoiceFieldSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer
|
||||
|
||||
|
||||
#
|
||||
@@ -71,6 +78,52 @@ class WritableTopologyMapSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
|
||||
|
||||
|
||||
#
|
||||
# Image attachments
|
||||
#
|
||||
|
||||
class ImageAttachmentSerializer(serializers.ModelSerializer):
|
||||
parent = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created']
|
||||
|
||||
def get_parent(self, obj):
|
||||
|
||||
# Static mapping of models to their nested serializers
|
||||
if isinstance(obj.parent, Device):
|
||||
serializer = NestedDeviceSerializer
|
||||
elif isinstance(obj.parent, Rack):
|
||||
serializer = NestedRackSerializer
|
||||
elif isinstance(obj.parent, Site):
|
||||
serializer = NestedSiteSerializer
|
||||
else:
|
||||
raise Exception("Unexpected type of parent object for ImageAttachment")
|
||||
|
||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||
|
||||
|
||||
class WritableImageAttachmentSerializer(serializers.ModelSerializer):
|
||||
content_type = ContentTypeFieldSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['id', 'content_type', 'object_id', 'name', 'image']
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
# Validate that the parent object exists
|
||||
try:
|
||||
data['content_type'].get_object_for_this_type(id=data['object_id'])
|
||||
except ObjectDoesNotExist:
|
||||
raise serializers.ValidationError(
|
||||
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
#
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
@@ -23,7 +25,11 @@ router.register(r'export-templates', views.ExportTemplateViewSet)
|
||||
# Topology maps
|
||||
router.register(r'topology-maps', views.TopologyMapViewSet)
|
||||
|
||||
# Image attachments
|
||||
router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
||||
|
||||
# Recent activity
|
||||
router.register(r'recent-activity', views.RecentActivityViewSet)
|
||||
|
||||
app_name = 'extras-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
@@ -6,7 +8,7 @@ from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from extras import filters
|
||||
from extras.models import ExportTemplate, Graph, TopologyMap, UserAction
|
||||
from extras.models import ExportTemplate, Graph, ImageAttachment, TopologyMap, UserAction
|
||||
from utilities.api import WritableSerializerMixin
|
||||
from . import serializers
|
||||
|
||||
@@ -51,7 +53,6 @@ class GraphViewSet(WritableSerializerMixin, ModelViewSet):
|
||||
class ExportTemplateViewSet(WritableSerializerMixin, ModelViewSet):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
serializer_class = serializers.ExportTemplateSerializer
|
||||
# write_serializer_class = serializers.WritableExportTemplateSerializer
|
||||
filter_class = filters.ExportTemplateFilter
|
||||
|
||||
|
||||
@@ -81,6 +82,12 @@ class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
|
||||
return response
|
||||
|
||||
|
||||
class ImageAttachmentViewSet(WritableSerializerMixin, ModelViewSet):
|
||||
queryset = ImageAttachment.objects.all()
|
||||
serializer_class = serializers.ImageAttachmentSerializer
|
||||
write_serializer_class = serializers.WritableImageAttachmentSerializer
|
||||
|
||||
|
||||
class RecentActivityViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
List all UserActions to provide a log of recent activity.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
@@ -30,7 +32,7 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
pass
|
||||
return queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__serialized_value=value,
|
||||
custom_field_values__serialized_value__icontains=value,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from utilities.forms import BulkEditForm, LaxURLField
|
||||
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
|
||||
from .models import (
|
||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
|
||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue,
|
||||
ImageAttachment,
|
||||
)
|
||||
|
||||
|
||||
@@ -103,7 +105,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
obj_id=self.instance.pk)
|
||||
except CustomFieldValue.DoesNotExist:
|
||||
# Skip this field if none exists already and its value is empty
|
||||
if self.cleaned_data[field_name] in [None, u'']:
|
||||
if self.cleaned_data[field_name] in [None, '']:
|
||||
continue
|
||||
cfv = CustomFieldValue(
|
||||
field=self.fields[field_name].model,
|
||||
@@ -158,3 +160,10 @@ class CustomFieldFilterForm(forms.Form):
|
||||
for name, field in custom_fields:
|
||||
field.required = False
|
||||
self.fields[name] = field
|
||||
|
||||
|
||||
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['name', 'image']
|
||||
|
||||
62
netbox/extras/management/commands/nbshell.py
Normal file
62
netbox/extras/management/commands/nbshell.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import code
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from django import get_version
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Model
|
||||
|
||||
|
||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users']
|
||||
|
||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
### Python {python} | Django {django} | NetBox {netbox}
|
||||
### lsmodels() will show available models. Use help(<model>) for more info.""".format(
|
||||
node=platform.node(),
|
||||
python=platform.python_version(),
|
||||
django=get_version(),
|
||||
netbox=settings.VERSION
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Start the Django shell with all NetBox models already imported"
|
||||
django_models = {}
|
||||
|
||||
def _lsmodels(self):
|
||||
for app, models in self.django_models.items():
|
||||
app_name = apps.get_app_config(app).verbose_name
|
||||
print('{}:'.format(app_name))
|
||||
for m in models:
|
||||
print(' {}'.format(m))
|
||||
|
||||
def get_namespace(self):
|
||||
namespace = {}
|
||||
|
||||
# Gather Django models from each app
|
||||
for app in APPS:
|
||||
self.django_models[app] = []
|
||||
app_models = sys.modules['{}.models'.format(app)]
|
||||
for name in dir(app_models):
|
||||
model = getattr(app_models, name)
|
||||
try:
|
||||
if issubclass(model, Model):
|
||||
namespace[name] = model
|
||||
self.django_models[app].append(name)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Load convenience commands
|
||||
namespace.update({
|
||||
'lsmodels': self._lsmodels,
|
||||
})
|
||||
|
||||
return namespace
|
||||
|
||||
def handle(self, **options):
|
||||
shell = code.interact(banner=BANNER_TEXT, local=self.get_namespace())
|
||||
return shell
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from getpass import getpass
|
||||
from ncclient.transport.errors import AuthenticationError
|
||||
from paramiko import AuthenticationException
|
||||
@@ -6,7 +8,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 +41,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 +74,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
|
||||
|
||||
20
netbox/extras/migrations/0005_useraction_add_bulk_create.py
Normal file
20
netbox/extras/migrations/0005_useraction_add_bulk_create.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-04-04 19:45
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0004_topologymap_change_comma_to_semicolon'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='useraction',
|
||||
name='action',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, b'created'), (7, b'bulk created'), (2, b'imported'), (3, b'modified'), (4, b'bulk edited'), (5, b'deleted'), (6, b'bulk deleted')]),
|
||||
),
|
||||
]
|
||||
34
netbox/extras/migrations/0006_add_imageattachments.py
Normal file
34
netbox/extras/migrations/0006_add_imageattachments.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-04-04 19:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import extras.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0005_useraction_add_bulk_create'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ImageAttachment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('image', models.ImageField(height_field=b'image_height', upload_to=extras.models.image_upload, width_field=b'image_width')),
|
||||
('image_height', models.PositiveSmallIntegerField()),
|
||||
('image_width', models.PositiveSmallIntegerField()),
|
||||
('name', models.CharField(blank=True, max_length=50)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
91
netbox/extras/migrations/0007_unicode_literals.py
Normal file
91
netbox/extras/migrations/0007_unicode_literals.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import extras.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0006_add_imageattachments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='default',
|
||||
field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='is_filterable',
|
||||
field=models.BooleanField(default=True, help_text='This field can be used to filter objects.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='required',
|
||||
field=models.BooleanField(default=False, help_text='Determines whether this field is required when creating new objects or editing an existing object.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='weight',
|
||||
field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfieldchoice',
|
||||
name='weight',
|
||||
field=models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='graph',
|
||||
name='link',
|
||||
field=models.URLField(blank=True, verbose_name='Link URL'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='graph',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='graph',
|
||||
name='source',
|
||||
field=models.CharField(max_length=500, verbose_name='Source URL'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='graph',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(choices=[(100, 'Interface'), (200, 'Provider'), (300, 'Site')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='imageattachment',
|
||||
name='image',
|
||||
field=models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='topologymap',
|
||||
name='device_patterns',
|
||||
field=models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useraction',
|
||||
name='action',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'created'), (7, 'bulk created'), (2, 'imported'), (3, 'modified'), (4, 'bulk edited'), (5, 'deleted'), (6, 'bulk deleted')]),
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
import graphviz
|
||||
@@ -13,6 +14,8 @@ from django.template import Template, Context
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from utilities.utils import foreground_color
|
||||
|
||||
|
||||
CUSTOMFIELD_MODELS = (
|
||||
'site', 'rack', 'devicetype', 'device', # DCIM
|
||||
@@ -58,13 +61,15 @@ ACTION_EDIT = 3
|
||||
ACTION_BULK_EDIT = 4
|
||||
ACTION_DELETE = 5
|
||||
ACTION_BULK_DELETE = 6
|
||||
ACTION_BULK_CREATE = 7
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_CREATE, 'created'),
|
||||
(ACTION_BULK_CREATE, 'bulk created'),
|
||||
(ACTION_IMPORT, 'imported'),
|
||||
(ACTION_EDIT, 'modified'),
|
||||
(ACTION_BULK_EDIT, 'bulk edited'),
|
||||
(ACTION_DELETE, 'deleted'),
|
||||
(ACTION_BULK_DELETE, 'bulk deleted')
|
||||
(ACTION_BULK_DELETE, 'bulk deleted'),
|
||||
)
|
||||
|
||||
|
||||
@@ -134,7 +139,11 @@ class CustomField(models.Model):
|
||||
if self.type == CF_TYPE_BOOLEAN:
|
||||
return str(int(bool(value)))
|
||||
if self.type == CF_TYPE_DATE:
|
||||
return value.strftime('%Y-%m-%d')
|
||||
# Could be date/datetime object or string
|
||||
try:
|
||||
return value.strftime('%Y-%m-%d')
|
||||
except AttributeError:
|
||||
return value
|
||||
if self.type == CF_TYPE_SELECT:
|
||||
# Could be ModelChoiceField or TypedChoiceField
|
||||
return str(value.id) if hasattr(value, 'id') else str(value)
|
||||
@@ -154,16 +163,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')
|
||||
@@ -174,7 +180,7 @@ class CustomFieldValue(models.Model):
|
||||
unique_together = ['field', 'obj_type', 'obj_id']
|
||||
|
||||
def __str__(self):
|
||||
return u'{} {}'.format(self.obj, self.field)
|
||||
return '{} {}'.format(self.obj, self.field)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
@@ -252,7 +258,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()
|
||||
@@ -266,7 +274,7 @@ class ExportTemplate(models.Model):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return u'{}: {}'.format(self.content_type, self.name)
|
||||
return '{}: {}'.format(self.content_type, self.name)
|
||||
|
||||
def to_response(self, context_dict, filename):
|
||||
"""
|
||||
@@ -292,7 +300,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. "
|
||||
@@ -314,7 +322,8 @@ class TopologyMap(models.Model):
|
||||
|
||||
def render(self, img_format='png'):
|
||||
|
||||
from dcim.models import Device, InterfaceConnection
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import CONNECTION_STATUS_CONNECTED, Device, InterfaceConnection
|
||||
|
||||
# Construct the graph
|
||||
graph = graphviz.Graph()
|
||||
@@ -332,9 +341,11 @@ 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)
|
||||
bg_color = '#{}'.format(d.device_role.color)
|
||||
fg_color = '#{}'.format(foreground_color(d.device_role.color))
|
||||
subgraph.node(d.name, style='filled', fillcolor=bg_color, fontcolor=fg_color, fontname='sans')
|
||||
|
||||
# Add an invisible connection to each successive device in a set to enforce horizontal order
|
||||
for j in range(0, len(devices) - 1):
|
||||
@@ -348,17 +359,90 @@ 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
|
||||
)
|
||||
for c in connections:
|
||||
graph.edge(c.interface_a.device.name, c.interface_b.device.name)
|
||||
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
|
||||
|
||||
# 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 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)
|
||||
|
||||
|
||||
#
|
||||
# Image attachments
|
||||
#
|
||||
|
||||
def image_upload(instance, filename):
|
||||
|
||||
path = 'image-attachments/'
|
||||
|
||||
# Rename the file to the provided name, if any. Attempt to preserve the file extension.
|
||||
extension = filename.rsplit('.')[-1].lower()
|
||||
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
|
||||
filename = '.'.join([instance.name, extension])
|
||||
elif instance.name:
|
||||
filename = instance.name
|
||||
|
||||
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ImageAttachment(models.Model):
|
||||
"""
|
||||
An uploaded image which is associated with an object.
|
||||
"""
|
||||
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')
|
||||
image_height = models.PositiveSmallIntegerField()
|
||||
image_width = models.PositiveSmallIntegerField()
|
||||
name = models.CharField(max_length=50, blank=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
filename = self.image.name.rsplit('/', 1)[-1]
|
||||
return filename.split('_', 2)[2]
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
_name = self.image.name
|
||||
|
||||
super(ImageAttachment, self).delete(*args, **kwargs)
|
||||
|
||||
# Delete file from disk
|
||||
self.image.delete(save=False)
|
||||
|
||||
# Deleting the file erases its name. We restore the image's filename here in case we still need to reference it
|
||||
# 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
|
||||
#
|
||||
@@ -396,6 +480,9 @@ class UserActionManager(models.Manager):
|
||||
def log_import(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_IMPORT, message)
|
||||
|
||||
def log_bulk_create(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_CREATE, message)
|
||||
|
||||
def log_bulk_edit(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_EDIT, message)
|
||||
|
||||
@@ -422,11 +509,11 @@ class UserAction(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
if self.message:
|
||||
return u'{} {}'.format(self.user, self.message)
|
||||
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
||||
return '{} {}'.format(self.user, self.message)
|
||||
return '{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
||||
|
||||
def icon(self):
|
||||
if self.action in [ACTION_CREATE, ACTION_IMPORT]:
|
||||
if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]:
|
||||
return mark_safe('<i class="glyphicon glyphicon-plus text-success"></i>')
|
||||
elif self.action in [ACTION_EDIT, ACTION_BULK_EDIT]:
|
||||
return mark_safe('<i class="glyphicon glyphicon-pencil text-warning"></i>')
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
import re
|
||||
import time
|
||||
|
||||
from ncclient import manager
|
||||
import paramiko
|
||||
import re
|
||||
import xmltodict
|
||||
import time
|
||||
|
||||
|
||||
CONNECT_TIMEOUT = 5 # seconds
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
from __future__ import unicode_literals
|
||||
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
|
||||
|
||||
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'])
|
||||
|
||||
15
netbox/extras/urls.py
Normal file
15
netbox/extras/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras import views
|
||||
|
||||
|
||||
app_name = 'extras'
|
||||
urlpatterns = [
|
||||
|
||||
# Image attachments
|
||||
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||
url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
||||
|
||||
]
|
||||
32
netbox/extras/views.py
Normal file
32
netbox/extras/views.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from utilities.views import ObjectDeleteView, ObjectEditView
|
||||
from .forms import ImageAttachmentForm
|
||||
from .models import ImageAttachment
|
||||
|
||||
|
||||
class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'extras.change_imageattachment'
|
||||
model = ImageAttachment
|
||||
form_class = ImageAttachmentForm
|
||||
|
||||
def alter_obj(self, imageattachment, request, args, kwargs):
|
||||
if not imageattachment.pk:
|
||||
# Assign the parent object based on URL kwargs
|
||||
model = kwargs.get('model')
|
||||
imageattachment.parent = get_object_or_404(model, pk=kwargs['object_id'])
|
||||
return imageattachment
|
||||
|
||||
def get_return_url(self, request, imageattachment):
|
||||
return imageattachment.parent.get_absolute_url()
|
||||
|
||||
|
||||
class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'extras.delete_imageattachment'
|
||||
model = ImageAttachment
|
||||
|
||||
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)))
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (
|
||||
Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(VRF)
|
||||
class VRFAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'rd', 'tenant', 'enforce_unique']
|
||||
list_filter = ['tenant']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(VRFAdmin, self).get_queryset(request)
|
||||
return qs.select_related('tenant')
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'weight']
|
||||
|
||||
|
||||
@admin.register(RIR)
|
||||
class RIRAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'is_private']
|
||||
|
||||
|
||||
@admin.register(Aggregate)
|
||||
class AggregateAdmin(admin.ModelAdmin):
|
||||
list_display = ['prefix', 'rir', 'date_added']
|
||||
list_filter = ['family', 'rir']
|
||||
search_fields = ['prefix']
|
||||
|
||||
|
||||
@admin.register(Prefix)
|
||||
class PrefixAdmin(admin.ModelAdmin):
|
||||
list_display = ['prefix', 'vrf', 'tenant', 'site', 'status', 'role', 'vlan']
|
||||
list_filter = ['family', 'site', 'status', 'role']
|
||||
search_fields = ['prefix']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(PrefixAdmin, self).get_queryset(request)
|
||||
return qs.select_related('vrf', 'site', 'role', 'vlan')
|
||||
|
||||
|
||||
@admin.register(IPAddress)
|
||||
class IPAddressAdmin(admin.ModelAdmin):
|
||||
list_display = ['address', 'vrf', 'tenant', 'nat_inside']
|
||||
list_filter = ['family']
|
||||
fields = ['address', 'vrf', 'device', 'interface', 'nat_inside']
|
||||
readonly_fields = ['interface', 'device', 'nat_inside']
|
||||
search_fields = ['address']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(IPAddressAdmin, self).get_queryset(request)
|
||||
return qs.select_related('vrf', 'nat_inside')
|
||||
|
||||
|
||||
@admin.register(VLANGroup)
|
||||
class VLANGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'site', 'slug']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(VLAN)
|
||||
class VLANAdmin(admin.ModelAdmin):
|
||||
list_display = ['site', 'vid', 'name', 'tenant', 'status', 'role']
|
||||
list_filter = ['site', 'tenant', 'status', 'role']
|
||||
search_fields = ['vid', 'name']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(VLANAdmin, self).get_queryset(request)
|
||||
return qs.select_related('site', 'tenant', 'role')
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
@@ -31,11 +33,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 +98,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']
|
||||
|
||||
|
||||
#
|
||||
@@ -135,7 +137,7 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
|
||||
# Validate uniqueness of name and slug if a site has been assigned.
|
||||
if data.get('site', None):
|
||||
for field in ['name', 'slug']:
|
||||
validator = UniqueTogetherValidator(queryset=VLAN.objects.all(), fields=('site', field))
|
||||
validator = UniqueTogetherValidator(queryset=VLANGroup.objects.all(), fields=('site', field))
|
||||
validator.set_context(self)
|
||||
validator(data)
|
||||
|
||||
@@ -169,11 +171,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 +218,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 +257,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']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
@@ -37,4 +39,5 @@ router.register(r'vlans', views.VLANViewSet)
|
||||
# Services
|
||||
router.register(r'services', views.ServiceViewSet)
|
||||
|
||||
app_name = 'ipam-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
@@ -34,6 +36,7 @@ class RoleViewSet(ModelViewSet):
|
||||
class RIRViewSet(ModelViewSet):
|
||||
queryset = RIR.objects.all()
|
||||
serializer_class = serializers.RIRSerializer
|
||||
filter_class = filters.RIRFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -99,3 +102,4 @@ class ServiceViewSet(WritableSerializerMixin, ModelViewSet):
|
||||
queryset = Service.objects.select_related('device')
|
||||
serializer_class = serializers.ServiceSerializer
|
||||
write_serializer_class = serializers.WritableServiceSerializer
|
||||
filter_class = filters.ServiceFilter
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import IPNetwork
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from netaddr import IPNetwork
|
||||
from netaddr.core import AddrFormatError
|
||||
@@ -8,8 +10,10 @@ from dcim.models import Site, Device, Interface
|
||||
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):
|
||||
@@ -81,7 +85,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||
except AddrFormatError:
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -153,10 +157,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():
|
||||
@@ -165,7 +172,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||
except AddrFormatError:
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -176,7 +183,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
query = str(IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix__net_contained_or_equal=query)
|
||||
except AddrFormatError:
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
|
||||
def filter_mask_length(self, queryset, name, value):
|
||||
@@ -237,10 +244,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():
|
||||
@@ -249,7 +259,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
ipaddress = str(IPNetwork(value.strip()))
|
||||
qs_filter |= Q(address__net_host=ipaddress)
|
||||
except AddrFormatError:
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -260,7 +270,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
query = str(IPNetwork(value.strip()).cidr)
|
||||
return queryset.filter(address__net_host_contained=query)
|
||||
except AddrFormatError:
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
|
||||
def filter_mask_length(self, queryset, name, value):
|
||||
@@ -337,10 +347,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():
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import IPNetwork, AddrFormatError
|
||||
|
||||
from django import forms
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Rack, Device, Interface
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch,
|
||||
SlugField, add_blank_choice,
|
||||
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField,
|
||||
ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, SlugField,
|
||||
add_blank_choice,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
||||
VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||
@@ -32,11 +36,11 @@ IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFForm(BootstrapMixin, CustomFieldForm):
|
||||
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant']
|
||||
labels = {
|
||||
'rd': "RD",
|
||||
}
|
||||
@@ -45,22 +49,31 @@ class VRFForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class VRFFromCSVForm(forms.ModelForm):
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
class VRFCSVForm(forms.ModelForm):
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
|
||||
|
||||
class VRFImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=VRFFromCSVForm)
|
||||
help_texts = {
|
||||
'name': 'VRF name',
|
||||
}
|
||||
|
||||
|
||||
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:
|
||||
@@ -110,19 +123,21 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class AggregateFromCSVForm(forms.ModelForm):
|
||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'RIR not found.'})
|
||||
class AggregateCSVForm(forms.ModelForm):
|
||||
rir = forms.ModelChoiceField(
|
||||
queryset=RIR.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent RIR',
|
||||
error_messages={
|
||||
'invalid_choice': 'RIR not found.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
||||
|
||||
|
||||
class AggregateImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=AggregateFromCSVForm)
|
||||
|
||||
|
||||
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
|
||||
@@ -160,89 +175,147 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
||||
widget=forms.Select(attrs={'filter-for': 'vlan', 'nullable': 'true'}))
|
||||
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
||||
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
|
||||
display_field='display_name'))
|
||||
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'vlan_group', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
vlan_group = ChainedModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
label='VLAN group',
|
||||
widget=APISelect(
|
||||
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
||||
attrs={'filter-for': 'vlan', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
vlan = ChainedModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
('group', 'vlan_group'),
|
||||
),
|
||||
required=False,
|
||||
label='VLAN',
|
||||
widget=APISelect(
|
||||
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'is_pool', 'description']
|
||||
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
if instance and instance.vlan is not None:
|
||||
initial['vlan_group'] = instance.vlan.group
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(PrefixForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
# Initialize field without choices to avoid pulling all VLANs from the database
|
||||
if self.is_bound and self.data.get('site'):
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=None)
|
||||
|
||||
|
||||
class PrefixFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
error_messages={'invalid_choice': 'VRF not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
vlan_group_name = forms.CharField(required=False)
|
||||
vlan_vid = forms.IntegerField(required=False)
|
||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid role.'})
|
||||
class PrefixCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
to_field_name='rd',
|
||||
help_text='Route distinguisher of parent VRF',
|
||||
error_messages={
|
||||
'invalid_choice': 'VRF not found.',
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site',
|
||||
error_messages={
|
||||
'invalid_choice': 'Site not found.',
|
||||
}
|
||||
)
|
||||
vlan_group = forms.CharField(
|
||||
help_text='Group name of assigned VLAN',
|
||||
required=False
|
||||
)
|
||||
vlan_vid = forms.IntegerField(
|
||||
help_text='Numeric ID of assigned VLAN',
|
||||
required=False
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=PREFIX_STATUS_CHOICES,
|
||||
help_text='Operational status'
|
||||
)
|
||||
role = forms.ModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Functional role',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid role.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'is_pool',
|
||||
'description']
|
||||
fields = [
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(PrefixFromCSVForm, self).clean()
|
||||
super(PrefixCSVForm, self).clean()
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
vlan_group_name = self.cleaned_data.get('vlan_group_name')
|
||||
vlan_group = self.cleaned_data.get('vlan_group')
|
||||
vlan_vid = self.cleaned_data.get('vlan_vid')
|
||||
|
||||
# Validate VLAN
|
||||
vlan_group = None
|
||||
if vlan_group_name:
|
||||
if vlan_group and vlan_vid:
|
||||
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:
|
||||
try:
|
||||
self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid)
|
||||
self.instance.vlan = VLAN.objects.get(site=site, group__name=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))
|
||||
except VLAN.MultipleObjectsReturned:
|
||||
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
|
||||
if site:
|
||||
raise forms.ValidationError("VLAN {} not found in site {} group {}".format(
|
||||
vlan_vid, site, vlan_group
|
||||
))
|
||||
else:
|
||||
raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group))
|
||||
except MultipleObjectsReturned:
|
||||
raise forms.ValidationError(
|
||||
"Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group)
|
||||
)
|
||||
elif vlan_vid:
|
||||
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Assign Prefix status by name
|
||||
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
|
||||
return super(PrefixFromCSVForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class PrefixImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=PrefixFromCSVForm)
|
||||
try:
|
||||
self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
|
||||
except VLAN.DoesNotExist:
|
||||
if site:
|
||||
raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
|
||||
else:
|
||||
raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid))
|
||||
except MultipleObjectsReturned:
|
||||
raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid))
|
||||
|
||||
|
||||
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -252,6 +325,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:
|
||||
@@ -262,7 +336,7 @@ def prefix_status_choices():
|
||||
status_counts = {}
|
||||
for status in Prefix.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 PREFIX_STATUS_CHOICES]
|
||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
||||
|
||||
|
||||
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
@@ -302,149 +376,247 @@ 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 Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'nat_inside', 'description']
|
||||
widgets = {
|
||||
'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IPAddressForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
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:
|
||||
self.initial['nat_site'] = self.instance.nat_inside.interface.device.site.pk
|
||||
self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(
|
||||
site=nat_inside.interface.device.site
|
||||
)
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
||||
interface__device=nat_inside.interface.device
|
||||
)
|
||||
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'])
|
||||
elif self.initial.get('nat_site'):
|
||||
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(
|
||||
interface__device__pk=self.data['nat_device'])
|
||||
elif self.initial.get('nat_device'):
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
||||
interface__device__pk=self.initial['nat_device'])
|
||||
else:
|
||||
self.fields['nat_inside'].choices = []
|
||||
|
||||
|
||||
class IPAddressBulkAddForm(BootstrapMixin, forms.Form):
|
||||
address = ExpandableIPAddressField()
|
||||
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)
|
||||
|
||||
|
||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(
|
||||
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm):
|
||||
interface_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack'}
|
||||
attrs={'filter-for': 'interface_rack'}
|
||||
)
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
interface_rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
chains=(
|
||||
('site', 'interface_site'),
|
||||
),
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
api_url='/api/dcim/racks/?site_id={{interface_site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
attrs={'filter-for': 'interface_device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
interface_device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device',
|
||||
chains=(
|
||||
('site', 'interface_site'),
|
||||
('rack', 'interface_rack'),
|
||||
),
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||
api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface'}
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
interface = ChainedModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
chains=(
|
||||
('device', 'interface_device'),
|
||||
),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
|
||||
)
|
||||
)
|
||||
nat_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'nat_rack'}
|
||||
)
|
||||
)
|
||||
nat_rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains=(
|
||||
('site', 'nat_site'),
|
||||
),
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{nat_site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'nat_device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
nat_device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains=(
|
||||
('site', 'nat_site'),
|
||||
('rack', 'nat_rack'),
|
||||
),
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{nat_site}}&rack_id={{nat_rack}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'nat_inside'}
|
||||
)
|
||||
)
|
||||
nat_inside = ChainedModelChoiceField(
|
||||
queryset=IPAddress.objects.all(),
|
||||
chains=(
|
||||
('interface__device', 'nat_device'),
|
||||
),
|
||||
required=False,
|
||||
label='IP Address',
|
||||
widget=APISelect(
|
||||
api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
|
||||
display_field='address'
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False,
|
||||
label='Search',
|
||||
widget=Livesearch(
|
||||
query_key='q',
|
||||
query_url='dcim-api:device_list',
|
||||
field_to_update='device'
|
||||
query_url='ipam-api:ipaddress-list',
|
||||
field_to_update='nat_inside',
|
||||
obj_label='address'
|
||||
)
|
||||
)
|
||||
interface = forms.ModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
label='Interface',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/{{device}}/interfaces/'
|
||||
)
|
||||
)
|
||||
set_as_primary = forms.BooleanField(
|
||||
label='Set as primary IP for device',
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(IPAddressAssignForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack'].choices = []
|
||||
self.fields['device'].choices = []
|
||||
self.fields['interface'].choices = []
|
||||
|
||||
|
||||
class IPAddressFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
error_messages={'invalid_choice': 'VRF not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in IPADDRESS_STATUS_CHOICES])
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
interface_name = forms.CharField(required=False)
|
||||
is_primary = forms.BooleanField(required=False)
|
||||
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_name', 'device', 'interface_name', 'is_primary', 'description']
|
||||
fields = [
|
||||
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
|
||||
'nat_inside', 'tenant_group', 'tenant',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
if instance and instance.interface is not None:
|
||||
initial['interface_site'] = instance.interface.device.site
|
||||
initial['interface_rack'] = instance.interface.device.rack
|
||||
initial['interface_device'] = instance.interface.device
|
||||
if instance and instance.nat_inside and instance.nat_inside.device is not None:
|
||||
initial['nat_site'] = instance.nat_inside.device.site
|
||||
initial['nat_rack'] = instance.nat_inside.device.rack
|
||||
initial['nat_device'] = instance.nat_inside.device
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(IPAddressForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
# 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
|
||||
|
||||
def clean(self):
|
||||
super(IPAddressForm, self).clean()
|
||||
|
||||
# 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 IPAddressPatternForm(BootstrapMixin, forms.Form):
|
||||
pattern = ExpandableIPAddressField(label='Address pattern')
|
||||
|
||||
|
||||
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'status', 'vrf', 'description', 'tenant_group', 'tenant']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
|
||||
class IPAddressCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
to_field_name='rd',
|
||||
help_text='Route distinguisher of the assigned VRF',
|
||||
error_messages={
|
||||
'invalid_choice': 'VRF not found.',
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Name of the assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=IPADDRESS_STATUS_CHOICES,
|
||||
help_text='Operational status'
|
||||
)
|
||||
device = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name or ID of assigned device',
|
||||
error_messages={
|
||||
'invalid_choice': 'Device not found.',
|
||||
}
|
||||
)
|
||||
interface_name = forms.CharField(
|
||||
help_text='Name of assigned interface',
|
||||
required=False
|
||||
)
|
||||
is_primary = forms.BooleanField(
|
||||
help_text='Make this the primary IP for the assigned device',
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(IPAddressCSVForm, self).clean()
|
||||
|
||||
device = self.cleaned_data.get('device')
|
||||
interface_name = self.cleaned_data.get('interface_name')
|
||||
@@ -453,39 +625,39 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
||||
# Validate interface
|
||||
if device and interface_name:
|
||||
try:
|
||||
Interface.objects.get(device=device, name=interface_name)
|
||||
self.instance.interface = Interface.objects.get(device=device, name=interface_name)
|
||||
except Interface.DoesNotExist:
|
||||
self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
|
||||
raise forms.ValidationError("Invalid interface {} for device {}".format(interface_name, device))
|
||||
elif device and not interface_name:
|
||||
self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
|
||||
raise forms.ValidationError("Device set ({}) but interface missing".format(device))
|
||||
elif interface_name and not device:
|
||||
self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
|
||||
raise forms.ValidationError("Interface set ({}) but device missing or invalid".format(interface_name))
|
||||
|
||||
# Validate is_primary
|
||||
if is_primary and not device:
|
||||
self.add_error('is_primary', "No device specified; cannot set as primary IP")
|
||||
raise forms.ValidationError("No device specified; cannot set as primary IP")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Assign status by name
|
||||
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
|
||||
# Set interface
|
||||
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
|
||||
self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['interface_name'])
|
||||
self.instance.interface = Interface.objects.get(
|
||||
device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['interface_name']
|
||||
)
|
||||
|
||||
ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs)
|
||||
|
||||
# Set as primary for device
|
||||
if self.cleaned_data['is_primary']:
|
||||
device = self.cleaned_data['device']
|
||||
if self.instance.address.version == 4:
|
||||
self.instance.primary_ip4_for = self.cleaned_data['device']
|
||||
device.primary_ip4 = ipaddress
|
||||
elif self.instance.address.version == 6:
|
||||
self.instance.primary_ip6_for = self.cleaned_data['device']
|
||||
device.primary_ip6 = ipaddress
|
||||
device.save()
|
||||
|
||||
return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class IPAddressImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
|
||||
return ipaddress
|
||||
|
||||
|
||||
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -503,7 +675,7 @@ def ipaddress_status_choices():
|
||||
status_counts = {}
|
||||
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
status_counts[status['status']] = status['count']
|
||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
|
||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
|
||||
|
||||
|
||||
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
@@ -552,14 +724,29 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
# VLANs
|
||||
#
|
||||
|
||||
class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
|
||||
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
||||
))
|
||||
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'group', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
group = ChainedModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
label='Group',
|
||||
widget=APISelect(
|
||||
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant']
|
||||
help_texts = {
|
||||
'site': "Leave blank if this VLAN spans multiple sites",
|
||||
'group': "VLAN group (optional)",
|
||||
@@ -568,49 +755,69 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
'status': "Operational status of this VLAN",
|
||||
'role': "The primary function of this VLAN",
|
||||
}
|
||||
widgets = {
|
||||
'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
|
||||
|
||||
|
||||
class VLANCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site',
|
||||
error_messages={
|
||||
'invalid_choice': 'Site not found.',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(VLANForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit VLAN group choices
|
||||
if self.is_bound and self.data.get('site'):
|
||||
self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['group'].queryset = VLANGroup.objects.filter(site=None)
|
||||
|
||||
|
||||
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.'})
|
||||
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.'})
|
||||
)
|
||||
group_name = forms.CharField(
|
||||
help_text='Name of VLAN group',
|
||||
required=False
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=VLAN_STATUS_CHOICES,
|
||||
help_text='Operational status'
|
||||
)
|
||||
role = forms.ModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Functional role',
|
||||
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', 'role', 'description']
|
||||
help_texts = {
|
||||
'vid': 'Numeric VLAN ID (1-4095)',
|
||||
'name': 'VLAN name',
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
m = super(VLANFromCSVForm, self).save(commit=False)
|
||||
# Assign VLAN status by name
|
||||
m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
if kwargs.get('commit'):
|
||||
m.save()
|
||||
return m
|
||||
def clean(self):
|
||||
|
||||
super(VLANCSVForm, self).clean()
|
||||
|
||||
class VLANImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=VLANFromCSVForm)
|
||||
site = self.cleaned_data.get('site')
|
||||
group_name = self.cleaned_data.get('group_name')
|
||||
|
||||
# Validate VLAN group
|
||||
if group_name:
|
||||
try:
|
||||
self.instance.group = VLANGroup.objects.get(site=site, name=group_name)
|
||||
except VLANGroup.DoesNotExist:
|
||||
if site:
|
||||
raise forms.ValidationError("VLAN group {} not found for site {}".format(group_name, site))
|
||||
else:
|
||||
raise forms.ValidationError("Global VLAN group {} not found".format(group_name))
|
||||
|
||||
|
||||
class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -623,14 +830,14 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['group', 'tenant', 'role', 'description']
|
||||
nullable_fields = ['site', 'group', 'tenant', 'role', 'description']
|
||||
|
||||
|
||||
def vlan_status_choices():
|
||||
status_counts = {}
|
||||
for status in VLAN.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 VLAN_STATUS_CHOICES]
|
||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
||||
|
||||
|
||||
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Lookup, Transform, IntegerField
|
||||
from django.db.models.lookups import BuiltinLookup
|
||||
|
||||
|
||||
133
netbox/ipam/migrations/0016_unicode_literals.py
Normal file
133
netbox/ipam/migrations/0016_unicode_literals.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import ipam.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0015_global_vlans'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='aggregate',
|
||||
name='family',
|
||||
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aggregate',
|
||||
name='rir',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aggregates', to='ipam.RIR', verbose_name='RIR'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='address',
|
||||
field=ipam.fields.IPAddressField(help_text='IPv4 or IPv6 address (with mask)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='family',
|
||||
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='nat_inside',
|
||||
field=models.OneToOneField(blank=True, help_text='The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name='NAT (Inside)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='vrf',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='ipam.VRF', verbose_name='VRF'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='family',
|
||||
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='is_pool',
|
||||
field=models.BooleanField(default=False, help_text='All IP addresses within this prefix are considered usable', verbose_name='Is a pool'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='prefix',
|
||||
field=ipam.fields.IPNetworkField(help_text='IPv4 or IPv6 network with mask'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, help_text='The primary function of this prefix', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'Container'), (1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, help_text='Operational status of this prefix', verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='vlan',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VLAN', verbose_name='VLAN'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='vrf',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VRF', verbose_name='VRF'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rir',
|
||||
name='is_private',
|
||||
field=models.BooleanField(default=False, help_text='IP space managed by this RIR is considered private', verbose_name='Private'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='ipaddresses',
|
||||
field=models.ManyToManyField(blank=True, related_name='services', to='ipam.IPAddress', verbose_name='IP addresses'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='port',
|
||||
field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name='Port number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='protocol',
|
||||
field=models.PositiveSmallIntegerField(choices=[(6, 'TCP'), (17, 'UDP')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vlan',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vlan',
|
||||
name='vid',
|
||||
field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)], verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vrf',
|
||||
name='enforce_unique',
|
||||
field=models.BooleanField(default=True, help_text='Prevent duplicate prefixes/IP addresses within this VRF', verbose_name='Enforce unique space'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vrf',
|
||||
name='rd',
|
||||
field=models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher'),
|
||||
),
|
||||
]
|
||||
@@ -1,12 +1,14 @@
|
||||
from netaddr import IPNetwork, cidr_merge
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import IPNetwork, IPSet
|
||||
|
||||
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
|
||||
@@ -15,7 +17,6 @@ from tenancy.models import Tenant
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
from utilities.sql import NullsFirstQuerySet
|
||||
from utilities.utils import csv_format
|
||||
|
||||
from .fields import IPNetworkField, IPAddressField
|
||||
|
||||
|
||||
@@ -88,6 +89,8 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = 'VRF'
|
||||
@@ -145,6 +148,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['prefix', 'rir', 'date_added', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['family', 'prefix']
|
||||
|
||||
@@ -199,15 +204,11 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
def get_utilization(self):
|
||||
"""
|
||||
Determine the utilization rate of the aggregate prefix and return it as a percentage.
|
||||
Determine the prefix utilization of the aggregate and return it as a percentage.
|
||||
"""
|
||||
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
|
||||
# Remove overlapping prefixes from list of children
|
||||
networks = cidr_merge([c.prefix for c in child_prefixes])
|
||||
children_size = float(0)
|
||||
for p in networks:
|
||||
children_size += p.size
|
||||
return int(children_size / self.prefix.size * 100)
|
||||
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
|
||||
child_prefixes = IPSet([p.prefix for p in queryset])
|
||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@@ -296,6 +297,10 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
objects = PrefixQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['vrf', 'family', 'prefix']
|
||||
verbose_name_plural = 'prefixes'
|
||||
@@ -306,9 +311,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:prefix', args=[self.pk])
|
||||
|
||||
def get_duplicates(self):
|
||||
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.prefix:
|
||||
@@ -356,6 +358,30 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.description,
|
||||
])
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
def get_duplicates(self):
|
||||
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
|
||||
|
||||
def get_utilization(self):
|
||||
"""
|
||||
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
|
||||
"container", calculate utilization based on child prefixes. For all others, count child IP addresses.
|
||||
"""
|
||||
if self.status == PREFIX_STATUS_CONTAINER:
|
||||
queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
|
||||
child_prefixes = IPSet([p.prefix for p in queryset])
|
||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||
else:
|
||||
child_count = IPAddress.objects.filter(
|
||||
address__net_contained_or_equal=str(self.prefix), vrf=self.vrf
|
||||
).count()
|
||||
prefix_size = self.prefix.size
|
||||
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
|
||||
prefix_size -= 2
|
||||
return int(float(child_count) / prefix_size * 100)
|
||||
|
||||
@property
|
||||
def new_subnet(self):
|
||||
if self.family == 4:
|
||||
@@ -367,9 +393,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
|
||||
return None
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
|
||||
class IPAddressManager(models.Manager):
|
||||
|
||||
@@ -413,6 +436,8 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
objects = IPAddressManager()
|
||||
|
||||
csv_headers = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['family', 'address']
|
||||
verbose_name = 'IP address'
|
||||
@@ -451,11 +476,12 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
def to_csv(self):
|
||||
|
||||
# Determine if this IP is primary for a Device
|
||||
is_primary = False
|
||||
if self.family == 4 and getattr(self, 'primary_ip4_for', False):
|
||||
is_primary = True
|
||||
elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
|
||||
is_primary = True
|
||||
else:
|
||||
is_primary = False
|
||||
|
||||
return csv_format([
|
||||
self.address,
|
||||
@@ -497,9 +523,7 @@ class VLANGroup(models.Model):
|
||||
verbose_name_plural = 'VLAN groups'
|
||||
|
||||
def __str__(self):
|
||||
if self.site is None:
|
||||
return self.name
|
||||
return u'{} - {}'.format(self.site.name, self.name)
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||
@@ -528,6 +552,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'group', 'vid']
|
||||
unique_together = [
|
||||
@@ -538,7 +564,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
verbose_name_plural = 'VLANs'
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name
|
||||
return self.display_name or super(VLAN, self).__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:vlan', args=[self.pk])
|
||||
@@ -565,7 +591,9 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return u'{} ({})'.format(self.vid, self.name)
|
||||
if self.vid and self.name:
|
||||
return "{} ({})".format(self.vid, self.name)
|
||||
return None
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
@@ -591,4 +619,4 @@ class Service(CreatedUpdatedModel):
|
||||
unique_together = ['device', 'protocol', 'port']
|
||||
|
||||
def __str__(self):
|
||||
return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
||||
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
@@ -33,7 +34,7 @@ RIR_ACTIONS = """
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph value %}
|
||||
{% if record.pk %}{% utilization_graph value %}{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
@@ -70,9 +71,18 @@ IPADDRESS_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||
{% elif perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
|
||||
{% else %}
|
||||
{{ record.0 }}
|
||||
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
IPADDRESS_DEVICE = """
|
||||
{% if record.interface %}
|
||||
<a href="{{ record.interface.device.get_absolute_url }}">{{ record.interface.device }}</a>
|
||||
({{ record.interface.name }})
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -133,16 +143,25 @@ TENANT_LINK = """
|
||||
|
||||
class VRFTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
|
||||
name = tables.LinkColumn()
|
||||
rd = tables.Column(verbose_name='RD')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
description = tables.Column(verbose_name='Description')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VRF
|
||||
fields = ('pk', 'name', 'rd', 'tenant', 'description')
|
||||
|
||||
|
||||
class VRFSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
rd = tables.Column(verbose_name='RD')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = VRF
|
||||
fields = ('name', 'rd', 'tenant', 'description')
|
||||
|
||||
|
||||
#
|
||||
# RIRs
|
||||
#
|
||||
@@ -177,18 +196,25 @@ class RIRTable(BaseTable):
|
||||
|
||||
class AggregateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
prefix = tables.LinkColumn('ipam:aggregate', args=[Accessor('pk')], verbose_name='Aggregate')
|
||||
rir = tables.Column(verbose_name='RIR')
|
||||
prefix = tables.LinkColumn(verbose_name='Aggregate')
|
||||
child_count = tables.Column(verbose_name='Prefixes')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
||||
description = tables.Column(verbose_name='Description')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Aggregate
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
|
||||
|
||||
|
||||
class AggregateSearchTable(SearchTable):
|
||||
prefix = tables.LinkColumn(verbose_name='Aggregate')
|
||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Aggregate
|
||||
fields = ('prefix', 'rir', 'date_added', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Roles
|
||||
#
|
||||
@@ -212,30 +238,29 @@ class RoleTable(BaseTable):
|
||||
|
||||
class PrefixTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix', attrs={'th': {'style': 'padding-left: 17px'}})
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
||||
role = tables.TemplateColumn(PREFIX_ROLE_LINK, verbose_name='Role')
|
||||
description = tables.Column(verbose_name='Description')
|
||||
role = tables.TemplateColumn(PREFIX_ROLE_LINK)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not record.pk else '',
|
||||
}
|
||||
|
||||
|
||||
class PrefixBriefTable(BaseTable):
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix')
|
||||
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
||||
role = tables.Column(verbose_name='Role')
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF)
|
||||
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')])
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
@@ -243,6 +268,20 @@ class PrefixBriefTable(BaseTable):
|
||||
orderable = False
|
||||
|
||||
|
||||
class PrefixSearchTable(SearchTable):
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
||||
role = tables.TemplateColumn(PREFIX_ROLE_LINK)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
|
||||
|
||||
#
|
||||
# IPAddresses
|
||||
#
|
||||
@@ -250,17 +289,17 @@ class PrefixBriefTable(BaseTable):
|
||||
class IPAddressTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
|
||||
verbose_name='Device')
|
||||
interface = tables.Column(orderable=False, verbose_name='Interface')
|
||||
description = tables.Column(verbose_name='Description')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
nat_inside = tables.LinkColumn(
|
||||
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
|
||||
)
|
||||
device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'nat_inside', 'device', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||
}
|
||||
@@ -268,17 +307,30 @@ class IPAddressTable(BaseTable):
|
||||
|
||||
class IPAddressBriefTable(BaseTable):
|
||||
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
|
||||
verbose_name='Device')
|
||||
interface = tables.Column(orderable=False, verbose_name='Interface')
|
||||
nat_inside = tables.LinkColumn('ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False,
|
||||
verbose_name='NAT (Inside)')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
|
||||
interface = tables.Column(orderable=False)
|
||||
nat_inside = tables.LinkColumn(
|
||||
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'device', 'interface', 'nat_inside')
|
||||
|
||||
|
||||
class IPAddressSearchTable(SearchTable):
|
||||
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
|
||||
interface = tables.Column(orderable=False)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||
|
||||
|
||||
#
|
||||
# VLAN groups
|
||||
#
|
||||
@@ -304,15 +356,26 @@ class VLANGroupTable(BaseTable):
|
||||
class VLANTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
name = tables.Column(verbose_name='Name')
|
||||
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
role = tables.TemplateColumn(VLAN_ROLE_LINK, verbose_name='Role')
|
||||
description = tables.Column(verbose_name='Description')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
|
||||
class VLANSearchTable(SearchTable):
|
||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from netaddr import IPNetwork
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import netaddr
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from ipam.models import IPAddress, Prefix, VRF
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class TestPrefix(TestCase):
|
||||
|
||||
@@ -1,79 +1,80 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'ipam'
|
||||
urlpatterns = [
|
||||
|
||||
# VRFs
|
||||
url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'),
|
||||
url(r'^vrfs/add/$', views.VRFEditView.as_view(), name='vrf_add'),
|
||||
url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'),
|
||||
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
||||
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
||||
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/$', views.vrf, name='vrf'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
|
||||
|
||||
# RIRs
|
||||
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
|
||||
url(r'^rirs/add/$', views.RIREditView.as_view(), name='rir_add'),
|
||||
url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'),
|
||||
url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
||||
url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
|
||||
|
||||
# Aggregates
|
||||
url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
|
||||
url(r'^aggregates/add/$', views.AggregateEditView.as_view(), name='aggregate_add'),
|
||||
url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'),
|
||||
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
||||
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
||||
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/$', views.aggregate, name='aggregate'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
|
||||
|
||||
# Roles
|
||||
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
|
||||
url(r'^roles/add/$', views.RoleEditView.as_view(), name='role_add'),
|
||||
url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'),
|
||||
url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
||||
url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
|
||||
|
||||
# Prefixes
|
||||
url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
|
||||
url(r'^prefixes/add/$', views.PrefixEditView.as_view(), name='prefix_add'),
|
||||
url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'),
|
||||
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
||||
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
||||
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/$', views.prefix, name='prefix'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.prefix_ipaddresses, name='prefix_ipaddresses'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
|
||||
|
||||
# IP addresses
|
||||
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
|
||||
url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'),
|
||||
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'),
|
||||
url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
|
||||
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
|
||||
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
||||
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||
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+)/$', views.IPAddressView.as_view(), 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
|
||||
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||
url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
|
||||
url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
|
||||
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||
|
||||
# VLANs
|
||||
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
|
||||
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),
|
||||
url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'),
|
||||
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
||||
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||
url(r'^vlans/(?P<pk>\d+)/$', views.vlan, name='vlan'),
|
||||
url(r'^vlans/(?P<pk>\d+)/$', views.VLANView.as_view(), name='vlan'),
|
||||
url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django_tables2 import RequestConfig
|
||||
import netaddr
|
||||
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.conf import settings
|
||||
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.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.views.generic import View
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
|
||||
from . import filters, forms, tables
|
||||
from .models import (
|
||||
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
|
||||
@@ -98,28 +98,34 @@ class VRFListView(ObjectListView):
|
||||
template_name = 'ipam/vrf_list.html'
|
||||
|
||||
|
||||
def vrf(request, pk):
|
||||
class VRFView(View):
|
||||
|
||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||
prefix_table = tables.PrefixBriefTable(
|
||||
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
|
||||
)
|
||||
prefix_table.exclude = ('vrf',)
|
||||
def get(self, request, pk):
|
||||
|
||||
return render(request, 'ipam/vrf.html', {
|
||||
'vrf': vrf,
|
||||
'prefix_table': prefix_table,
|
||||
})
|
||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||
prefix_table = tables.PrefixBriefTable(
|
||||
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
|
||||
)
|
||||
prefix_table.exclude = ('vrf',)
|
||||
|
||||
return render(request, 'ipam/vrf.html', {
|
||||
'vrf': vrf,
|
||||
'prefix_table': prefix_table,
|
||||
})
|
||||
|
||||
|
||||
class VRFEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_vrf'
|
||||
class VRFCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_vrf'
|
||||
model = VRF
|
||||
form_class = forms.VRFForm
|
||||
template_name = 'ipam/vrf_edit.html'
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFEditView(VRFCreateView):
|
||||
permission_required = 'ipam.change_vrf'
|
||||
|
||||
|
||||
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'ipam.delete_vrf'
|
||||
model = VRF
|
||||
@@ -128,9 +134,8 @@ class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_vrf'
|
||||
form = forms.VRFImportForm
|
||||
model_form = forms.VRFCSVForm
|
||||
table = tables.VRFTable
|
||||
template_name = 'ipam/vrf_import.html'
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
@@ -238,15 +243,19 @@ class RIRListView(ObjectListView):
|
||||
}
|
||||
|
||||
|
||||
class RIREditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_rir'
|
||||
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_rir'
|
||||
model = RIR
|
||||
form_class = forms.RIRForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('ipam:rir_list')
|
||||
|
||||
|
||||
class RIREditView(RIRCreateView):
|
||||
permission_required = 'ipam.change_rir'
|
||||
|
||||
|
||||
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_rir'
|
||||
cls = RIR
|
||||
@@ -283,42 +292,58 @@ class AggregateListView(ObjectListView):
|
||||
}
|
||||
|
||||
|
||||
def aggregate(request, pk):
|
||||
class AggregateView(View):
|
||||
|
||||
aggregate = get_object_or_404(Aggregate, pk=pk)
|
||||
def get(self, request, pk):
|
||||
|
||||
# Find all child prefixes contained by this aggregate
|
||||
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\
|
||||
.select_related('site', 'role').annotate_depth(limit=0)
|
||||
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
|
||||
aggregate = get_object_or_404(Aggregate, pk=pk)
|
||||
|
||||
prefix_table = tables.PrefixTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
||||
# Find all child prefixes contained by this aggregate
|
||||
child_prefixes = Prefix.objects.filter(
|
||||
prefix__net_contained_or_equal=str(aggregate.prefix)
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
).annotate_depth(
|
||||
limit=0
|
||||
)
|
||||
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_prefix'),
|
||||
'change': request.user.has_perm('ipam.change_prefix'),
|
||||
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||
}
|
||||
prefix_table = tables.PrefixTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table.base_columns['pk'].visible = True
|
||||
|
||||
return render(request, 'ipam/aggregate.html', {
|
||||
'aggregate': aggregate,
|
||||
'prefix_table': prefix_table,
|
||||
'permissions': permissions,
|
||||
})
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(prefix_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_prefix'),
|
||||
'change': request.user.has_perm('ipam.change_prefix'),
|
||||
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||
}
|
||||
|
||||
return render(request, 'ipam/aggregate.html', {
|
||||
'aggregate': aggregate,
|
||||
'prefix_table': prefix_table,
|
||||
'permissions': permissions,
|
||||
})
|
||||
|
||||
|
||||
class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_aggregate'
|
||||
class AggregateCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_aggregate'
|
||||
model = Aggregate
|
||||
form_class = forms.AggregateForm
|
||||
template_name = 'ipam/aggregate_edit.html'
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateEditView(AggregateCreateView):
|
||||
permission_required = 'ipam.change_aggregate'
|
||||
|
||||
|
||||
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'ipam.delete_aggregate'
|
||||
model = Aggregate
|
||||
@@ -327,9 +352,8 @@ class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_aggregate'
|
||||
form = forms.AggregateImportForm
|
||||
model_form = forms.AggregateCSVForm
|
||||
table = tables.AggregateTable
|
||||
template_name = 'ipam/aggregate_import.html'
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
@@ -359,15 +383,19 @@ class RoleListView(ObjectListView):
|
||||
template_name = 'ipam/role_list.html'
|
||||
|
||||
|
||||
class RoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_role'
|
||||
class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_role'
|
||||
model = Role
|
||||
form_class = forms.RoleForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('ipam:role_list')
|
||||
|
||||
|
||||
class RoleEditView(RoleCreateView):
|
||||
permission_required = 'ipam.change_role'
|
||||
|
||||
|
||||
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_role'
|
||||
cls = Role
|
||||
@@ -391,72 +419,134 @@ class PrefixListView(ObjectListView):
|
||||
return self.queryset.annotate_depth(limit=limit)
|
||||
|
||||
|
||||
def prefix(request, pk):
|
||||
class PrefixView(View):
|
||||
|
||||
prefix = get_object_or_404(Prefix.objects.select_related(
|
||||
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
|
||||
), pk=pk)
|
||||
def get(self, request, pk):
|
||||
|
||||
try:
|
||||
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
|
||||
except Aggregate.DoesNotExist:
|
||||
aggregate = None
|
||||
prefix = get_object_or_404(Prefix.objects.select_related(
|
||||
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
|
||||
), pk=pk)
|
||||
|
||||
# Count child IP addresses
|
||||
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
|
||||
.count()
|
||||
try:
|
||||
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
|
||||
except Aggregate.DoesNotExist:
|
||||
aggregate = None
|
||||
|
||||
# Parent prefixes table
|
||||
parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
|
||||
.filter(prefix__net_contains=str(prefix.prefix))\
|
||||
.select_related('site', 'role').annotate_depth()
|
||||
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
||||
parent_prefix_table.exclude = ('vrf',)
|
||||
# Count child IP addresses
|
||||
ipaddress_count = IPAddress.objects.filter(
|
||||
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
|
||||
).count()
|
||||
|
||||
# Duplicate prefixes table
|
||||
duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
|
||||
.select_related('site', 'role')
|
||||
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
|
||||
duplicate_prefix_table.exclude = ('vrf',)
|
||||
# Parent prefixes table
|
||||
parent_prefixes = Prefix.objects.filter(
|
||||
Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
|
||||
).filter(
|
||||
prefix__net_contains=str(prefix.prefix)
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
).annotate_depth()
|
||||
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
||||
parent_prefix_table.exclude = ('vrf',)
|
||||
|
||||
# Child prefixes table
|
||||
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\
|
||||
.select_related('site', 'role').annotate_depth(limit=0)
|
||||
if child_prefixes:
|
||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||
child_prefix_table = tables.PrefixTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
child_prefix_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
||||
# Duplicate prefixes table
|
||||
duplicate_prefixes = Prefix.objects.filter(
|
||||
vrf=prefix.vrf, prefix=str(prefix.prefix)
|
||||
).exclude(
|
||||
pk=prefix.pk
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
)
|
||||
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
|
||||
duplicate_prefix_table.exclude = ('vrf',)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_prefix'),
|
||||
'change': request.user.has_perm('ipam.change_prefix'),
|
||||
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||
}
|
||||
# Child prefixes table
|
||||
child_prefixes = Prefix.objects.filter(
|
||||
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
).annotate_depth(limit=0)
|
||||
if child_prefixes:
|
||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||
child_prefix_table = tables.PrefixTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
child_prefix_table.base_columns['pk'].visible = True
|
||||
|
||||
return render(request, 'ipam/prefix.html', {
|
||||
'prefix': prefix,
|
||||
'aggregate': aggregate,
|
||||
'ipaddress_count': ipaddress_count,
|
||||
'parent_prefix_table': parent_prefix_table,
|
||||
'child_prefix_table': child_prefix_table,
|
||||
'duplicate_prefix_table': duplicate_prefix_table,
|
||||
'permissions': permissions,
|
||||
'return_url': prefix.get_absolute_url(),
|
||||
})
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(child_prefix_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_prefix'),
|
||||
'change': request.user.has_perm('ipam.change_prefix'),
|
||||
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||
}
|
||||
|
||||
return render(request, 'ipam/prefix.html', {
|
||||
'prefix': prefix,
|
||||
'aggregate': aggregate,
|
||||
'ipaddress_count': ipaddress_count,
|
||||
'parent_prefix_table': parent_prefix_table,
|
||||
'child_prefix_table': child_prefix_table,
|
||||
'duplicate_prefix_table': duplicate_prefix_table,
|
||||
'permissions': permissions,
|
||||
'return_url': prefix.get_absolute_url(),
|
||||
})
|
||||
|
||||
|
||||
class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_prefix'
|
||||
class PrefixIPAddressesView(View):
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
||||
|
||||
# Find all IPAddresses belonging to this Prefix
|
||||
ipaddresses = IPAddress.objects.filter(
|
||||
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
|
||||
).select_related(
|
||||
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
|
||||
)
|
||||
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
|
||||
|
||||
ip_table = tables.IPAddressTable(ipaddresses)
|
||||
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||
ip_table.base_columns['pk'].visible = True
|
||||
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(ip_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_ipaddress'),
|
||||
'change': request.user.has_perm('ipam.change_ipaddress'),
|
||||
'delete': request.user.has_perm('ipam.delete_ipaddress'),
|
||||
}
|
||||
|
||||
return render(request, 'ipam/prefix_ipaddresses.html', {
|
||||
'prefix': prefix,
|
||||
'ip_table': ip_table,
|
||||
'permissions': permissions,
|
||||
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
|
||||
})
|
||||
|
||||
|
||||
class PrefixCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_prefix'
|
||||
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'
|
||||
|
||||
|
||||
class PrefixEditView(PrefixCreateView):
|
||||
permission_required = 'ipam.change_prefix'
|
||||
|
||||
|
||||
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'ipam.delete_prefix'
|
||||
model = Prefix
|
||||
@@ -466,9 +556,8 @@ class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_prefix'
|
||||
form = forms.PrefixImportForm
|
||||
model_form = forms.PrefixCSVForm
|
||||
table = tables.PrefixTable
|
||||
template_name = 'ipam/prefix_import.html'
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
@@ -488,186 +577,94 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
def prefix_ipaddresses(request, pk):
|
||||
|
||||
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
||||
|
||||
# Find all IPAddresses belonging to this Prefix
|
||||
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
|
||||
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
||||
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
|
||||
|
||||
ip_table = tables.IPAddressTable(ipaddresses)
|
||||
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||
ip_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_ipaddress'),
|
||||
'change': request.user.has_perm('ipam.change_ipaddress'),
|
||||
'delete': request.user.has_perm('ipam.delete_ipaddress'),
|
||||
}
|
||||
|
||||
return render(request, 'ipam/prefix_ipaddresses.html', {
|
||||
'prefix': prefix,
|
||||
'ip_table': ip_table,
|
||||
'permissions': permissions,
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressListView(ObjectListView):
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
|
||||
filter = filters.IPAddressFilter
|
||||
filter_form = forms.IPAddressFilterForm
|
||||
table = tables.IPAddressTable
|
||||
template_name = 'ipam/ipaddress_list.html'
|
||||
|
||||
|
||||
def ipaddress(request, pk):
|
||||
class IPAddressView(View):
|
||||
|
||||
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
|
||||
def get(self, request, pk):
|
||||
|
||||
# Parent prefixes table
|
||||
parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\
|
||||
.select_related('site', 'role')
|
||||
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
|
||||
parent_prefixes_table.exclude = ('vrf',)
|
||||
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
|
||||
|
||||
# Duplicate IPs table
|
||||
duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\
|
||||
.exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside')
|
||||
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
|
||||
# Parent prefixes table
|
||||
parent_prefixes = Prefix.objects.filter(
|
||||
vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
)
|
||||
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
|
||||
parent_prefixes_table.exclude = ('vrf',)
|
||||
|
||||
# Related IP table
|
||||
related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\
|
||||
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
||||
related_ips_table = tables.IPAddressBriefTable(list(related_ips))
|
||||
# Duplicate IPs table
|
||||
duplicate_ips = IPAddress.objects.filter(
|
||||
vrf=ipaddress.vrf, address=str(ipaddress.address)
|
||||
).exclude(
|
||||
pk=ipaddress.pk
|
||||
).select_related(
|
||||
'interface__device', 'nat_inside'
|
||||
)
|
||||
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
|
||||
|
||||
return render(request, 'ipam/ipaddress.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'parent_prefixes_table': parent_prefixes_table,
|
||||
'duplicate_ips_table': duplicate_ips_table,
|
||||
'related_ips_table': related_ips_table,
|
||||
})
|
||||
# Related IP table
|
||||
related_ips = IPAddress.objects.select_related(
|
||||
'interface__device'
|
||||
).exclude(
|
||||
address=str(ipaddress.address)
|
||||
).filter(
|
||||
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
||||
)
|
||||
related_ips_table = tables.IPAddressBriefTable(list(related_ips))
|
||||
|
||||
return render(request, 'ipam/ipaddress.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'parent_prefixes_table': parent_prefixes_table,
|
||||
'duplicate_ips_table': duplicate_ips_table,
|
||||
'related_ips_table': related_ips_table,
|
||||
})
|
||||
|
||||
|
||||
@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'
|
||||
class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
model = IPAddress
|
||||
form_class = forms.IPAddressForm
|
||||
fields_initial = ['address', 'vrf']
|
||||
template_name = 'ipam/ipaddress_edit.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressEditView(IPAddressCreateView):
|
||||
permission_required = 'ipam.change_ipaddress'
|
||||
|
||||
|
||||
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'ipam.delete_ipaddress'
|
||||
model = IPAddress
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
||||
class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
form = forms.IPAddressBulkAddForm
|
||||
model = IPAddress
|
||||
pattern_form = forms.IPAddressPatternForm
|
||||
model_form = forms.IPAddressBulkAddForm
|
||||
pattern_target = 'address'
|
||||
template_name = 'ipam/ipaddress_bulk_add.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
form = forms.IPAddressImportForm
|
||||
model_form = forms.IPAddressCSVForm
|
||||
table = tables.IPAddressTable
|
||||
template_name = 'ipam/ipaddress_import.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
def save_obj(self, obj):
|
||||
obj.save()
|
||||
|
||||
# Update primary IP for device if needed. The Device must be updated directly in the database; otherwise we risk
|
||||
# overwriting a previous IP assignment from the same import (see #861).
|
||||
try:
|
||||
if obj.family == 4 and obj.primary_ip4_for:
|
||||
Device.objects.filter(pk=obj.primary_ip4_for.pk).update(primary_ip4=obj)
|
||||
elif obj.family == 6 and obj.primary_ip6_for:
|
||||
Device.objects.filter(pk=obj.primary_ip6_for.pk).update(primary_ip6=obj)
|
||||
except Device.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_ipaddress'
|
||||
@@ -697,15 +694,19 @@ class VLANGroupListView(ObjectListView):
|
||||
template_name = 'ipam/vlangroup_list.html'
|
||||
|
||||
|
||||
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_vlangroup'
|
||||
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_vlangroup'
|
||||
model = VLANGroup
|
||||
form_class = forms.VLANGroupForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('ipam:vlangroup_list')
|
||||
|
||||
|
||||
class VLANGroupEditView(VLANGroupCreateView):
|
||||
permission_required = 'ipam.change_vlangroup'
|
||||
|
||||
|
||||
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vlangroup'
|
||||
cls = VLANGroup
|
||||
@@ -725,27 +726,35 @@ class VLANListView(ObjectListView):
|
||||
template_name = 'ipam/vlan_list.html'
|
||||
|
||||
|
||||
def vlan(request, pk):
|
||||
class VLANView(View):
|
||||
|
||||
vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk)
|
||||
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
|
||||
prefix_table = tables.PrefixBriefTable(list(prefixes))
|
||||
prefix_table.exclude = ('vlan',)
|
||||
def get(self, request, pk):
|
||||
|
||||
return render(request, 'ipam/vlan.html', {
|
||||
'vlan': vlan,
|
||||
'prefix_table': prefix_table,
|
||||
})
|
||||
vlan = get_object_or_404(VLAN.objects.select_related(
|
||||
'site__region', 'tenant__group', 'role'
|
||||
), pk=pk)
|
||||
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
|
||||
prefix_table = tables.PrefixBriefTable(list(prefixes))
|
||||
prefix_table.exclude = ('vlan',)
|
||||
|
||||
return render(request, 'ipam/vlan.html', {
|
||||
'vlan': vlan,
|
||||
'prefix_table': prefix_table,
|
||||
})
|
||||
|
||||
|
||||
class VLANEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_vlan'
|
||||
class VLANCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_vlan'
|
||||
model = VLAN
|
||||
form_class = forms.VLANForm
|
||||
template_name = 'ipam/vlan_edit.html'
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANEditView(VLANCreateView):
|
||||
permission_required = 'ipam.change_vlan'
|
||||
|
||||
|
||||
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'ipam.delete_vlan'
|
||||
model = VLAN
|
||||
@@ -754,9 +763,8 @@ class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_vlan'
|
||||
form = forms.VLANImportForm
|
||||
model_form = forms.VLANCSVForm
|
||||
table = tables.VLANTable
|
||||
template_name = 'ipam/vlan_import.html'
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
@@ -780,8 +788,8 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Services
|
||||
#
|
||||
|
||||
class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_service'
|
||||
class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.add_service'
|
||||
model = Service
|
||||
form_class = forms.ServiceForm
|
||||
template_name = 'ipam/service_edit.html'
|
||||
@@ -791,10 +799,14 @@ 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()
|
||||
|
||||
|
||||
class ServiceEditView(ServiceCreateView):
|
||||
permission_required = 'ipam.change_service'
|
||||
|
||||
|
||||
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'ipam.delete_service'
|
||||
model = Service
|
||||
|
||||
2
netbox/media/image-attachments/.gitignore
vendored
Normal file
2
netbox/media/image-attachments/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -58,6 +58,11 @@ CORS_ORIGIN_REGEX_WHITELIST = [
|
||||
# r'^(https?://)?(\w+\.)?example\.com$',
|
||||
]
|
||||
|
||||
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
|
||||
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
|
||||
# on a production system.
|
||||
DEBUG = False
|
||||
|
||||
# Email settings
|
||||
EMAIL = {
|
||||
'SERVER': 'localhost',
|
||||
@@ -72,6 +77,10 @@ EMAIL = {
|
||||
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
|
||||
ENFORCE_GLOBAL_UNIQUE = False
|
||||
|
||||
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
|
||||
# https://docs.djangoproject.com/en/1.11/topics/logging/
|
||||
LOGGING = {}
|
||||
|
||||
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
|
||||
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||
LOGIN_REQUIRED = False
|
||||
@@ -79,6 +88,11 @@ LOGIN_REQUIRED = False
|
||||
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||
MAINTENANCE_MODE = False
|
||||
|
||||
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
|
||||
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
|
||||
# all objects by specifying "?limit=0".
|
||||
MAX_PAGE_SIZE = 1000
|
||||
|
||||
# Credentials that NetBox will use to access live devices (future use).
|
||||
NETBOX_USERNAME = ''
|
||||
NETBOX_PASSWORD = ''
|
||||
|
||||
42
netbox/netbox/forms.py
Normal file
42
netbox/netbox/forms.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
|
||||
OBJ_TYPE_CHOICES = (
|
||||
('', 'All Objects'),
|
||||
('Circuits', (
|
||||
('provider', 'Providers'),
|
||||
('circuit', 'Circuits'),
|
||||
)),
|
||||
('DCIM', (
|
||||
('site', 'Sites'),
|
||||
('rack', 'Racks'),
|
||||
('devicetype', 'Device types'),
|
||||
('device', 'Devices'),
|
||||
)),
|
||||
('IPAM', (
|
||||
('vrf', 'VRFs'),
|
||||
('aggregate', 'Aggregates'),
|
||||
('prefix', 'Prefixes'),
|
||||
('ipaddress', 'IP addresses'),
|
||||
('vlan', 'VLANs'),
|
||||
)),
|
||||
('Secrets', (
|
||||
('secret', 'Secrets'),
|
||||
)),
|
||||
('Tenancy', (
|
||||
('tenant', 'Tenants'),
|
||||
)),
|
||||
)
|
||||
|
||||
|
||||
class SearchForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
label='Query', widget=forms.TextInput(attrs={'style': 'width: 350px'})
|
||||
)
|
||||
obj_type = forms.ChoiceField(
|
||||
choices=OBJ_TYPE_CHOICES, required=False, label='Type'
|
||||
)
|
||||
@@ -13,9 +13,9 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.0-beta1'
|
||||
VERSION = '2.0.10'
|
||||
|
||||
# Import local configuration
|
||||
# Import required configuration parameters
|
||||
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
try:
|
||||
@@ -25,32 +25,35 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
"Mandatory setting {} is missing from configuration.py.".format(setting)
|
||||
)
|
||||
|
||||
# Default configurations
|
||||
# Import optional configuration parameters
|
||||
ADMINS = getattr(configuration, 'ADMINS', [])
|
||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
|
||||
if BASE_PATH:
|
||||
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
|
||||
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
|
||||
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
||||
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
||||
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
||||
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
LOGGING = getattr(configuration, 'LOGGING', {})
|
||||
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
||||
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
|
||||
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
|
||||
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
|
||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
|
||||
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
|
||||
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
|
||||
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
|
||||
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
||||
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
||||
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
||||
|
||||
# Attempt to import LDAP configuration if it has been defined
|
||||
@@ -112,6 +115,7 @@ INSTALLED_APPS = (
|
||||
'django.contrib.humanize',
|
||||
'corsheaders',
|
||||
'debug_toolbar',
|
||||
'django_filters',
|
||||
'django_tables2',
|
||||
'mptt',
|
||||
'rest_framework',
|
||||
@@ -153,6 +157,7 @@ TEMPLATES = [
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.template.context_processors.media',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'utilities.context_processors.settings',
|
||||
@@ -167,19 +172,21 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
USE_X_FORWARDED_HOST = True
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
||||
STATIC_ROOT = BASE_DIR + '/static/'
|
||||
STATIC_URL = '/{}static/'.format(BASE_PATH)
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, "project-static"),
|
||||
)
|
||||
|
||||
# Media
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
MEDIA_URL = '/{}media/'.format(BASE_PATH)
|
||||
|
||||
# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = None
|
||||
|
||||
@@ -204,7 +211,7 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'rest_framework.filters.DjangoFilterBackend',
|
||||
),
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'DEFAULT_PAGINATION_CLASS': 'utilities.api.OptionalLimitOffsetPagination',
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'utilities.api.TokenPermissions',
|
||||
),
|
||||
@@ -215,6 +222,14 @@ REST_FRAMEWORK = {
|
||||
}
|
||||
|
||||
# Django debug toolbar
|
||||
# Disable the templates panel by default due to a performance issue in Django 1.11; see
|
||||
# https://github.com/jazzband/django-debug-toolbar/issues/910
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
'DISABLE_PANELS': [
|
||||
'debug_toolbar.panels.redirects.RedirectsPanel',
|
||||
'debug_toolbar.panels.templates.TemplatesPanel',
|
||||
],
|
||||
}
|
||||
INTERNAL_IPS = (
|
||||
'127.0.0.1',
|
||||
'::1',
|
||||
|
||||
@@ -1,45 +1,56 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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
|
||||
from django.views.static import serve
|
||||
|
||||
from netbox.views import APIRootView, home, handle_500, trigger_500
|
||||
from users.views import login, logout
|
||||
from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500
|
||||
from users.views import LoginView, LogoutView
|
||||
|
||||
|
||||
handler500 = handle_500
|
||||
swagger_view = get_swagger_view(title='NetBox API')
|
||||
|
||||
_patterns = [
|
||||
|
||||
# Default page
|
||||
url(r'^$', home, name='home'),
|
||||
# Base views
|
||||
url(r'^$', HomeView.as_view(), name='home'),
|
||||
url(r'^search/$', SearchView.as_view(), name='search'),
|
||||
|
||||
# Login/logout
|
||||
url(r'^login/$', login, name='login'),
|
||||
url(r'^logout/$', logout, name='logout'),
|
||||
url(r'^login/$', LoginView.as_view(), name='login'),
|
||||
url(r'^logout/$', LogoutView.as_view(), name='logout'),
|
||||
|
||||
# Apps
|
||||
url(r'^circuits/', include('circuits.urls', namespace='circuits')),
|
||||
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
|
||||
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}),
|
||||
|
||||
# Error testing
|
||||
url(r'^500/$', trigger_500),
|
||||
|
||||
# Admin
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^admin/', admin.site.urls),
|
||||
|
||||
]
|
||||
|
||||
|
||||
@@ -1,55 +1,208 @@
|
||||
from __future__ import unicode_literals
|
||||
from collections import OrderedDict
|
||||
import sys
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import View
|
||||
|
||||
from circuits.models import Provider, Circuit
|
||||
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
|
||||
from extras.models import UserAction
|
||||
from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
|
||||
from circuits.filters import CircuitFilter, ProviderFilter
|
||||
from circuits.models import Circuit, Provider
|
||||
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 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
|
||||
from secrets.filters import SecretFilter
|
||||
from secrets.models import Secret
|
||||
from secrets.tables import SecretSearchTable
|
||||
from tenancy.filters import TenantFilter
|
||||
from tenancy.models import Tenant
|
||||
from tenancy.tables import TenantSearchTable
|
||||
from .forms import SearchForm
|
||||
|
||||
|
||||
def home(request):
|
||||
SEARCH_MAX_RESULTS = 15
|
||||
SEARCH_TYPES = OrderedDict((
|
||||
# Circuits
|
||||
('provider', {
|
||||
'queryset': Provider.objects.all(),
|
||||
'filter': ProviderFilter,
|
||||
'table': ProviderSearchTable,
|
||||
'url': 'circuits:provider_list',
|
||||
}),
|
||||
('circuit', {
|
||||
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
|
||||
'filter': CircuitFilter,
|
||||
'table': CircuitSearchTable,
|
||||
'url': 'circuits:circuit_list',
|
||||
}),
|
||||
# DCIM
|
||||
('site', {
|
||||
'queryset': Site.objects.select_related('region', 'tenant'),
|
||||
'filter': SiteFilter,
|
||||
'table': SiteSearchTable,
|
||||
'url': 'dcim:site_list',
|
||||
}),
|
||||
('rack', {
|
||||
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
|
||||
'filter': RackFilter,
|
||||
'table': RackSearchTable,
|
||||
'url': 'dcim:rack_list',
|
||||
}),
|
||||
('devicetype', {
|
||||
'queryset': DeviceType.objects.select_related('manufacturer'),
|
||||
'filter': DeviceTypeFilter,
|
||||
'table': DeviceTypeSearchTable,
|
||||
'url': 'dcim:devicetype_list',
|
||||
}),
|
||||
('device', {
|
||||
'queryset': Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'),
|
||||
'filter': DeviceFilter,
|
||||
'table': DeviceSearchTable,
|
||||
'url': 'dcim:device_list',
|
||||
}),
|
||||
# IPAM
|
||||
('vrf', {
|
||||
'queryset': VRF.objects.select_related('tenant'),
|
||||
'filter': VRFFilter,
|
||||
'table': VRFSearchTable,
|
||||
'url': 'ipam:vrf_list',
|
||||
}),
|
||||
('aggregate', {
|
||||
'queryset': Aggregate.objects.select_related('rir'),
|
||||
'filter': AggregateFilter,
|
||||
'table': AggregateSearchTable,
|
||||
'url': 'ipam:aggregate_list',
|
||||
}),
|
||||
('prefix', {
|
||||
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
|
||||
'filter': PrefixFilter,
|
||||
'table': PrefixSearchTable,
|
||||
'url': 'ipam:prefix_list',
|
||||
}),
|
||||
('ipaddress', {
|
||||
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
|
||||
'filter': IPAddressFilter,
|
||||
'table': IPAddressSearchTable,
|
||||
'url': 'ipam:ipaddress_list',
|
||||
}),
|
||||
('vlan', {
|
||||
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
|
||||
'filter': VLANFilter,
|
||||
'table': VLANSearchTable,
|
||||
'url': 'ipam:vlan_list',
|
||||
}),
|
||||
# Secrets
|
||||
('secret', {
|
||||
'queryset': Secret.objects.select_related('role', 'device'),
|
||||
'filter': SecretFilter,
|
||||
'table': SecretSearchTable,
|
||||
'url': 'secrets:secret_list',
|
||||
}),
|
||||
# Tenancy
|
||||
('tenant', {
|
||||
'queryset': Tenant.objects.select_related('group'),
|
||||
'filter': TenantFilter,
|
||||
'table': TenantSearchTable,
|
||||
'url': 'tenancy:tenant_list',
|
||||
}),
|
||||
))
|
||||
|
||||
stats = {
|
||||
|
||||
# Organization
|
||||
'site_count': Site.objects.count(),
|
||||
'tenant_count': Tenant.objects.count(),
|
||||
class HomeView(View):
|
||||
template_name = 'home.html'
|
||||
|
||||
# DCIM
|
||||
'rack_count': Rack.objects.count(),
|
||||
'device_count': Device.objects.count(),
|
||||
'interface_connections_count': InterfaceConnection.objects.count(),
|
||||
'console_connections_count': ConsolePort.objects.filter(cs_port__isnull=False).count(),
|
||||
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
|
||||
def get(self, request):
|
||||
|
||||
# IPAM
|
||||
'vrf_count': VRF.objects.count(),
|
||||
'aggregate_count': Aggregate.objects.count(),
|
||||
'prefix_count': Prefix.objects.count(),
|
||||
'ipaddress_count': IPAddress.objects.count(),
|
||||
'vlan_count': VLAN.objects.count(),
|
||||
stats = {
|
||||
|
||||
# Circuits
|
||||
'provider_count': Provider.objects.count(),
|
||||
'circuit_count': Circuit.objects.count(),
|
||||
# Organization
|
||||
'site_count': Site.objects.count(),
|
||||
'tenant_count': Tenant.objects.count(),
|
||||
|
||||
# Secrets
|
||||
'secret_count': Secret.objects.count(),
|
||||
# DCIM
|
||||
'rack_count': Rack.objects.count(),
|
||||
'device_count': Device.objects.count(),
|
||||
'interface_connections_count': InterfaceConnection.objects.count(),
|
||||
'console_connections_count': ConsolePort.objects.filter(cs_port__isnull=False).count(),
|
||||
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
|
||||
|
||||
}
|
||||
# IPAM
|
||||
'vrf_count': VRF.objects.count(),
|
||||
'aggregate_count': Aggregate.objects.count(),
|
||||
'prefix_count': Prefix.objects.count(),
|
||||
'ipaddress_count': IPAddress.objects.count(),
|
||||
'vlan_count': VLAN.objects.count(),
|
||||
|
||||
return render(request, 'home.html', {
|
||||
'stats': stats,
|
||||
'recent_activity': UserAction.objects.select_related('user')[:50]
|
||||
})
|
||||
# Circuits
|
||||
'provider_count': Provider.objects.count(),
|
||||
'circuit_count': Circuit.objects.count(),
|
||||
|
||||
# Secrets
|
||||
'secret_count': Secret.objects.count(),
|
||||
|
||||
}
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'search_form': SearchForm(),
|
||||
'stats': stats,
|
||||
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
|
||||
'recent_activity': UserAction.objects.select_related('user')[:50]
|
||||
})
|
||||
|
||||
|
||||
class SearchView(View):
|
||||
|
||||
def get(self, request):
|
||||
|
||||
# No query
|
||||
if 'q' not in request.GET:
|
||||
return render(request, 'search.html', {
|
||||
'form': SearchForm(),
|
||||
})
|
||||
|
||||
form = SearchForm(request.GET)
|
||||
results = []
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
# Searching for a single type of object
|
||||
if form.cleaned_data['obj_type']:
|
||||
obj_types = [form.cleaned_data['obj_type']]
|
||||
# Searching all object types
|
||||
else:
|
||||
obj_types = SEARCH_TYPES.keys()
|
||||
|
||||
for obj_type in obj_types:
|
||||
|
||||
queryset = SEARCH_TYPES[obj_type]['queryset']
|
||||
filter_cls = SEARCH_TYPES[obj_type]['filter']
|
||||
table = SEARCH_TYPES[obj_type]['table']
|
||||
url = SEARCH_TYPES[obj_type]['url']
|
||||
|
||||
# Construct the results table for this object type
|
||||
filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs
|
||||
table = table(filtered_queryset)
|
||||
table.paginate(per_page=SEARCH_MAX_RESULTS)
|
||||
|
||||
if table.page:
|
||||
results.append({
|
||||
'name': queryset.model._meta.verbose_name_plural,
|
||||
'table': table,
|
||||
'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q'])
|
||||
})
|
||||
|
||||
return render(request, 'search.html', {
|
||||
'form': form,
|
||||
'results': results,
|
||||
})
|
||||
|
||||
|
||||
class APIRootView(APIView):
|
||||
@@ -57,7 +210,7 @@ class APIRootView(APIView):
|
||||
exclude_from_schema = True
|
||||
|
||||
def get_view_name(self):
|
||||
return u"API Root"
|
||||
return "API Root"
|
||||
|
||||
def get(self, request, format=None):
|
||||
|
||||
@@ -86,5 +239,6 @@ def trigger_500(request):
|
||||
"""
|
||||
Hot-wired method of triggering a server error to test reporting
|
||||
"""
|
||||
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
|
||||
"person you are.")
|
||||
raise Exception(
|
||||
"Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are."
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user