mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-09 02:17:44 +01:00
Compare commits
227 Commits
v2.7-beta1
...
v2.6.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
946779000f | ||
|
|
c3c3000a53 | ||
|
|
66daeda85f | ||
|
|
0fd0e76183 | ||
|
|
cf480375f6 | ||
|
|
736cd709d9 | ||
|
|
0f42219b4b | ||
|
|
b64f4b93eb | ||
|
|
32ffc1b54b | ||
|
|
608006ee77 | ||
|
|
3d78a67343 | ||
|
|
2f98f133bb | ||
|
|
fe0fbeab49 | ||
|
|
49fa243b4f | ||
|
|
a2308b9c99 | ||
|
|
422c6bad5b | ||
|
|
b7e78028ce | ||
|
|
509a115f68 | ||
|
|
f20d16f188 | ||
|
|
f4514034b8 | ||
|
|
6bc8f2e50b | ||
|
|
fc1245c49d | ||
|
|
4be7ca0c78 | ||
|
|
cb91c9231d | ||
|
|
a5413a5484 | ||
|
|
71120d9899 | ||
|
|
acb66c7dc0 | ||
|
|
2eba84dad5 | ||
|
|
fe89982d4e | ||
|
|
0296aa240a | ||
|
|
d88b3456c4 | ||
|
|
789cf827f2 | ||
|
|
6c19c88e99 | ||
|
|
c1ed2b6068 | ||
|
|
4eacc57522 | ||
|
|
1e740a70f7 | ||
|
|
94a7d8e493 | ||
|
|
883655ce71 | ||
|
|
6019260374 | ||
|
|
e5c5a1a101 | ||
|
|
c13b9d8798 | ||
|
|
03b10b6f73 | ||
|
|
67f4d8fab5 | ||
|
|
7e0073d6f5 | ||
|
|
c66dca399b | ||
|
|
9d085ad83a | ||
|
|
ad565e55f1 | ||
|
|
46c712e735 | ||
|
|
989d6f5af3 | ||
|
|
86865b91f8 | ||
|
|
b53480dd6a | ||
|
|
581ed52b24 | ||
|
|
472486acd6 | ||
|
|
92ec4bd22f | ||
|
|
959a0da0ed | ||
|
|
b23eaeca54 | ||
|
|
4f9271e9ff | ||
|
|
094553dbe7 | ||
|
|
60e4812b32 | ||
|
|
40625d1299 | ||
|
|
c9ec8b71e0 | ||
|
|
73e456495f | ||
|
|
54227ca9c7 | ||
|
|
f3b323536e | ||
|
|
6537f35176 | ||
|
|
b36d0ca3fc | ||
|
|
99809109ab | ||
|
|
1cdbfd6d60 | ||
|
|
4030e5ec24 | ||
|
|
4151e52802 | ||
|
|
b1e8145ffb | ||
|
|
c04d8ca5a7 | ||
|
|
e312c30822 | ||
|
|
40c30baffa | ||
|
|
bca7435a5a | ||
|
|
396bb28967 | ||
|
|
eb40275427 | ||
|
|
8519f546a6 | ||
|
|
e9b2ad9f5c | ||
|
|
39fba4f05d | ||
|
|
38c16d71b4 | ||
|
|
8fef6edb27 | ||
|
|
5cac900380 | ||
|
|
f49467bcb5 | ||
|
|
98a66f7fbe | ||
|
|
ce8d470860 | ||
|
|
dc475f4755 | ||
|
|
a7982bb0e1 | ||
|
|
ea05b5b606 | ||
|
|
996d49de67 | ||
|
|
74997a18a5 | ||
|
|
832fd49339 | ||
|
|
acb2f32304 | ||
|
|
32f39e10c9 | ||
|
|
190e683654 | ||
|
|
770f4c962c | ||
|
|
227921e0a0 | ||
|
|
39d0261d8a | ||
|
|
f267a532f6 | ||
|
|
e7ee4486a5 | ||
|
|
6a3cd83efc | ||
|
|
a7ec0c14f7 | ||
|
|
05bfe94d3e | ||
|
|
8d0aaa4ec1 | ||
|
|
4b5c4b7be5 | ||
|
|
18333973aa | ||
|
|
07a1baef13 | ||
|
|
15545b70d6 | ||
|
|
cfb8b3cf56 | ||
|
|
206732eb62 | ||
|
|
258cc4b50e | ||
|
|
d1f81783ef | ||
|
|
9bd2af48a3 | ||
|
|
d4df965f46 | ||
|
|
7dddd4734c | ||
|
|
c45daca5f2 | ||
|
|
067af26892 | ||
|
|
792f38334a | ||
|
|
dba40cd6bc | ||
|
|
4b19073b8b | ||
|
|
28eca9a026 | ||
|
|
3556051d14 | ||
|
|
e1d1f522ff | ||
|
|
04f3e58ab4 | ||
|
|
e1c61c5019 | ||
|
|
1c0de0093b | ||
|
|
9d8ab81e3a | ||
|
|
fa55571503 | ||
|
|
feb04f0401 | ||
|
|
5c07b6dc1d | ||
|
|
e3b448b7ad | ||
|
|
57f199f899 | ||
|
|
b38bb64c81 | ||
|
|
c2dc243c7c | ||
|
|
25c3c1b431 | ||
|
|
156fbae7d3 | ||
|
|
a0ae7a227d | ||
|
|
9ef3e68479 | ||
|
|
435d248645 | ||
|
|
53db5090c1 | ||
|
|
caa062c8ba | ||
|
|
240bbc2944 | ||
|
|
a3861ed492 | ||
|
|
82c70302fd | ||
|
|
80d1f80b61 | ||
|
|
d3c6caf8a8 | ||
|
|
a707204f98 | ||
|
|
523a1388db | ||
|
|
970586b07b | ||
|
|
28ae6849b4 | ||
|
|
8e3a371688 | ||
|
|
2a219eff23 | ||
|
|
f81641ae96 | ||
|
|
4f0d3e6b32 | ||
|
|
77d1ac8b07 | ||
|
|
5d0ac02704 | ||
|
|
fc5d07bb13 | ||
|
|
395f23e1d3 | ||
|
|
dd85448451 | ||
|
|
6a7af22dea | ||
|
|
37bc17d3a2 | ||
|
|
ca131e5b2a | ||
|
|
c75795ceda | ||
|
|
7dc0591e3e | ||
|
|
cfa078c929 | ||
|
|
57ea73db46 | ||
|
|
7ad9e8a2fb | ||
|
|
1a57120b78 | ||
|
|
d267aeb621 | ||
|
|
a110b0badb | ||
|
|
242ae9eb91 | ||
|
|
53625e0dea | ||
|
|
aa4f73ffbf | ||
|
|
8c7b0cf670 | ||
|
|
05570ae4ad | ||
|
|
0cf94cff16 | ||
|
|
b5455ed882 | ||
|
|
8a4293a4cc | ||
|
|
f649b9f04f | ||
|
|
5caa04ef2b | ||
|
|
f2c49063f8 | ||
|
|
ea2351e902 | ||
|
|
8effb54c89 | ||
|
|
66c99acb9e | ||
|
|
2e5a326315 | ||
|
|
b5177c608d | ||
|
|
be7d5a2310 | ||
|
|
5230127fde | ||
|
|
405320d8ab | ||
|
|
2f2e193cf9 | ||
|
|
1fc206b9c5 | ||
|
|
44e1a477f2 | ||
|
|
e62302c979 | ||
|
|
51b0fe4596 | ||
|
|
7399aa0c5e | ||
|
|
aa4588f9ba | ||
|
|
4c2e2e0fa3 | ||
|
|
6dea8ddbce | ||
|
|
e6623a6ca8 | ||
|
|
0c5f535689 | ||
|
|
ae9d0d894a | ||
|
|
14401c30b6 | ||
|
|
fbb93c72d0 | ||
|
|
5fedcd1f4e | ||
|
|
adeee0bf5c | ||
|
|
84a2b726f5 | ||
|
|
407a60dcc4 | ||
|
|
d31507985b | ||
|
|
0174c9747b | ||
|
|
55b503da5b | ||
|
|
aff4ad0f97 | ||
|
|
50df3acd26 | ||
|
|
e53e1e31de | ||
|
|
1acdf58a4b | ||
|
|
1d1cb867cd | ||
|
|
b46bfaebc1 | ||
|
|
a22c7c1539 | ||
|
|
ea51aa97b7 | ||
|
|
6a6959d041 | ||
|
|
462cede863 | ||
|
|
85c11bbd83 | ||
|
|
77e0564d13 | ||
|
|
3b03d68ac7 | ||
|
|
b57d64c72d | ||
|
|
3b76e0203a | ||
|
|
425670f52a | ||
|
|
9f7313e492 |
23
.github/lock.yml
vendored
Normal file
23
.github/lock.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Configuration for Lock (https://github.com/apps/lock)
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 90
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: 2020-01-01
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: false
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: true
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
# only: issues
|
||||
7
.github/stale.yml
vendored
7
.github/stale.yml
vendored
@@ -1,20 +1,27 @@
|
||||
# Configuration for Stale (https://github.com/apps/stale)
|
||||
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 14
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- "status: accepted"
|
||||
- "status: gathering feedback"
|
||||
- "status: blocked"
|
||||
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. NetBox
|
||||
is governed by a small group of core maintainers which means not all opened
|
||||
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
|
||||
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
This issue has been automatically closed due to lack of activity. In an
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,9 +12,5 @@
|
||||
fabfile.py
|
||||
*.swp
|
||||
gunicorn_config.py
|
||||
gunicorn.conf
|
||||
netbox.log
|
||||
netbox.pid
|
||||
.DS_Store
|
||||
.vscode
|
||||
.coverage
|
||||
|
||||
@@ -10,7 +10,6 @@ python:
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install pycodestyle
|
||||
- pip install coverage
|
||||
before_script:
|
||||
- psql --version
|
||||
- psql -U postgres -c 'SELECT version();'
|
||||
|
||||
@@ -16,7 +16,7 @@ For real-time discussion, you can join the #netbox Slack channel on [NetworkToCo
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
* First, ensure that you've installed the [latest stable version](https://github.com/netbox-community/netbox/releases)
|
||||
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases)
|
||||
of NetBox. If you're running an older version, it's possible that the bug has
|
||||
already been fixed.
|
||||
|
||||
@@ -28,27 +28,26 @@ up (+1). You might also want to add a comment describing how it's affecting your
|
||||
installation. This will allow us to prioritize bugs based on how many users are
|
||||
affected.
|
||||
|
||||
* If you haven't found an existing issue that describes your suspected bug,
|
||||
please inquire about it on the mailing list. **Do not** file an issue until you
|
||||
have received confirmation that it is in fact a bug. Invalid issues are very
|
||||
distracting and slow the pace at which NetBox is developed.
|
||||
|
||||
* When submitting an issue, please be as descriptive as possible. Be sure to
|
||||
include:
|
||||
provide all information request in the issue template, including:
|
||||
|
||||
* The environment in which NetBox is running
|
||||
* The exact steps that can be taken to reproduce the issue (if applicable)
|
||||
* The exact steps that can be taken to reproduce the issue
|
||||
* Expected and observed behavior
|
||||
* 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
|
||||
The issue will be reviewed by a maintainer after submission and the appropriate
|
||||
labels will be applied for categorization.
|
||||
|
||||
* 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.
|
||||
|
||||
* For more information on how bug reports are handled, please see our [issue
|
||||
intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy).
|
||||
|
||||
## Feature Requests
|
||||
|
||||
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues)
|
||||
@@ -61,10 +60,10 @@ free to add a comment with any additional justification for the feature.
|
||||
(However, note that comments with no substance other than a "+1" will be
|
||||
deleted. Please use GitHub's reactions feature to indicate your support.)
|
||||
|
||||
* Due to an excessive backlog of feature requests, we are not currently
|
||||
accepting any proposals which substantially extend NetBox's functionality
|
||||
beyond its current feature set. This includes the introduction of any new views
|
||||
or models which have not already been proposed in an existing feature request.
|
||||
* Due to a large backlog of feature requests, we are not currently accepting
|
||||
any proposals which substantially extend NetBox's functionality beyond its
|
||||
current feature set. This includes the introduction of any new views or models
|
||||
which have not already been proposed in an existing feature request.
|
||||
|
||||
* Before filing a new feature request, consider raising your idea on the
|
||||
mailing list first. Feedback you receive there will help validate and shape the
|
||||
@@ -75,8 +74,8 @@ describe the functionality and data model(s) being proposed. The more effort
|
||||
you put into writing a feature request, the better its chance is of being
|
||||
implemented. Overly broad feature requests will be closed.
|
||||
|
||||
* When submitting a feature request on GitHub, be sure to include the
|
||||
following:
|
||||
* When submitting a feature request on GitHub, be sure to include all
|
||||
information requested by the issue template, including:
|
||||
|
||||
* A detailed description of the proposed functionality
|
||||
* A use case for the feature; who would use it and what value it would add
|
||||
@@ -89,6 +88,9 @@ following:
|
||||
title. The issue will be reviewed by a moderator after submission and the
|
||||
appropriate labels will be applied for categorization.
|
||||
|
||||
* For more information on how feature requests are handled, please see our
|
||||
[issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy).
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
* Be sure to open an issue **before** starting work on a pull request, and
|
||||
@@ -103,7 +105,7 @@ any work that's already in progress.
|
||||
|
||||
* When submitting a pull request, please be sure to work off of the `develop`
|
||||
branch, rather than `master`. The `develop` branch is used for ongoing
|
||||
development, while `master` is used for tagging new stable releases.
|
||||
development, while `master` is used for tagging stable releases.
|
||||
|
||||
* All code submissions should meet the following criteria (CI will enforce
|
||||
these checks):
|
||||
@@ -122,27 +124,26 @@ reduce noise in the discussion.
|
||||
|
||||
## Issue Lifecycle
|
||||
|
||||
When a correctly formatted issue is submitted it is evaluated by a moderator
|
||||
who may elect to immediately label the issue as accepted in addition to another
|
||||
issue type label. In other cases, the issue may be labeled as "status: gathering feedback"
|
||||
which will often be accompanied by a comment from a moderator asking for further dialog from the community.
|
||||
If an issue is labeled as "status: revisions needed" a moderator has identified a problem with
|
||||
the issue itself and is asking for the submitter himself to update the original post with
|
||||
the requested information. If the original post is not updated in a reasonable amount of time,
|
||||
the issue will be closed as invalid.
|
||||
New issues are handled according to our [issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy).
|
||||
Maintainers will assign label(s) and/or close new issues as the policy
|
||||
dictates. This helps ensure a productive development environment and avoid
|
||||
accumulating a large backlog of work.
|
||||
|
||||
The core maintainers group has chosen to make use of the GitHub Stale bot to aid in issue management.
|
||||
The core maintainers group has chosen to make use of GitHub's [Stale bot](https://github.com/apps/stale)
|
||||
to aid in issue management.
|
||||
|
||||
* Issues will be marked as stale after 14 days of no activity.
|
||||
* Then after 7 more days of inactivity, the issue will be closed.
|
||||
* Any issue bearing one of the following labels will be exempt from all Stale bot actions:
|
||||
* Any issue bearing one of the following labels will be exempt from all Stale
|
||||
bot actions:
|
||||
* `status: accepted`
|
||||
* `status: gathering feedback`
|
||||
* `status: blocked`
|
||||
|
||||
It is natural that some new issues get more attention than others. Often this is a metric of an issues's
|
||||
overall usefulness to the project. In other cases in which issues merely get lost in the shuffle,
|
||||
notifications from Stale bot can bring renewed attention to potentially meaningful issues.
|
||||
It is natural that some new issues get more attention than others. Often this
|
||||
is a metric of an issues's overall value to the project. In other cases in
|
||||
which issues merely get lost in the shuffle, notifications from Stale bot can
|
||||
bring renewed attention to potentially meaningful issues.
|
||||
|
||||
## Maintainer Guidance
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
NetBox is an IP address management (IPAM) and data center infrastructure
|
||||
management (DCIM) tool. Initially conceived by the network engineering team at
|
||||
|
||||
@@ -22,14 +22,14 @@ django-filter
|
||||
# https://github.com/django-mptt/django-mptt
|
||||
django-mptt
|
||||
|
||||
# Prometheus metrics library for Django
|
||||
# https://github.com/korfuri/django-prometheus
|
||||
django-prometheus
|
||||
|
||||
# Django integration for RQ (Reqis queuing)
|
||||
# https://github.com/rq/django-rq
|
||||
django-rq
|
||||
|
||||
# Prometheus metrics library for Django
|
||||
# https://github.com/korfuri/django-prometheus
|
||||
django-prometheus
|
||||
|
||||
# Abstraction models for rendering and paginating HTML tables
|
||||
# https://github.com/jieter/django-tables2
|
||||
django-tables2
|
||||
@@ -54,6 +54,10 @@ djangorestframework
|
||||
# https://github.com/axnsan12/drf-yasg
|
||||
drf-yasg[validation]
|
||||
|
||||
# Python interface to the graphviz graph rendering utility
|
||||
# https://github.com/xflr6/graphviz
|
||||
graphviz
|
||||
|
||||
# Simple markup language for rendering HTML
|
||||
# https://github.com/Python-Markdown/markdown
|
||||
# py-gfm requires Markdown<3.0
|
||||
@@ -78,11 +82,3 @@ py-gfm
|
||||
# Extensive cryptographic library (fork of pycrypto)
|
||||
# https://github.com/Legrandin/pycryptodome
|
||||
pycryptodome
|
||||
|
||||
# In-memory key/value store used for caching and queuing
|
||||
# https://github.com/andymccurdy/redis-py
|
||||
redis
|
||||
|
||||
# Python Package to write SVG files - used for rack elevations
|
||||
# https://github.com/mozman/svgwrite
|
||||
svgwrite
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# The IP address (typically localhost) and port that the Netbox WSGI process should listen on
|
||||
bind = '127.0.0.1:8001'
|
||||
|
||||
# Number of gunicorn workers to spawn. This should typically be 2n+1, where
|
||||
# n is the number of CPU cores present.
|
||||
workers = 5
|
||||
|
||||
# Number of threads per worker process
|
||||
threads = 3
|
||||
|
||||
# Timeout (in seconds) for a request to complete
|
||||
timeout = 120
|
||||
|
||||
# The maximum number of requests a worker can handle before being respawned
|
||||
max_requests = 5000
|
||||
max_requests_jitter = 500
|
||||
@@ -1,22 +0,0 @@
|
||||
[Unit]
|
||||
Description=NetBox Request Queue Worker
|
||||
Documentation=https://netbox.readthedocs.io/en/stable/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
User=www-data
|
||||
Group=www-data
|
||||
|
||||
WorkingDirectory=/opt/netbox
|
||||
|
||||
ExecStart=/usr/bin/python3 /opt/netbox/netbox/manage.py rqworker
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,22 +0,0 @@
|
||||
[Unit]
|
||||
Description=NetBox WSGI Service
|
||||
Documentation=https://netbox.readthedocs.io/en/stable/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
User=www-data
|
||||
Group=www-data
|
||||
PIDFile=/var/tmp/netbox.pid
|
||||
WorkingDirectory=/opt/netbox
|
||||
|
||||
ExecStart=/usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -71,6 +71,18 @@ The checkbox to commit database changes when executing a script is checked by de
|
||||
commit_default = False
|
||||
```
|
||||
|
||||
## Accessing Request Data
|
||||
|
||||
Details of the current HTTP request (the one being made to execute the script) are available as the instance attribute `self.request`. This can be used to infer, for example, the user executing the script and the client IP address:
|
||||
|
||||
```python
|
||||
username = self.request.user.username
|
||||
ip_address = self.request.META.get('HTTP_X_FORWARDED_FOR') or self.request.META.get('REMOTE_ADDR')
|
||||
self.log_info("Running as user {} (IP: {})...".format(username, ip_address))
|
||||
```
|
||||
|
||||
For a complete list of available request parameters, please see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/request-response/).
|
||||
|
||||
## Reading Data from Files
|
||||
|
||||
The Script class provides two convenience methods for reading data from files:
|
||||
|
||||
65
docs/additional-features/napalm.md
Normal file
65
docs/additional-features/napalm.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# NAPALM
|
||||
|
||||
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API.
|
||||
|
||||
!!! info
|
||||
To enable the integration, the NAPALM library must be installed. See [installation steps](../../installation/2-netbox/#napalm-automation-optional) for more information.
|
||||
|
||||
```
|
||||
GET /api/dcim/devices/1/napalm/?method=get_environment
|
||||
|
||||
{
|
||||
"get_environment": {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
By default, the [`NAPALM_USERNAME`](../../configuration/optional-settings/#napalm_username) and [`NAPALM_PASSWORD`](../../configuration/optional-settings/#napalm_password) are used for NAPALM authentication. They can be overridden for an individual API call through the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
|
||||
|
||||
```
|
||||
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
|
||||
-H "Authorization: Token f4b378553dacfcfd44c5a0b9ae49b57e29c552b5" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-NAPALM-Username: foo" \
|
||||
-H "X-NAPALM-Password: bar"
|
||||
```
|
||||
|
||||
## Method Support
|
||||
|
||||
The list of supported NAPALM methods depends on the [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html#general-support-matrix) configured for the platform of a device. NetBox only supports [get](https://napalm.readthedocs.io/en/latest/support/index.html#getters-support-matrix) methods.
|
||||
|
||||
## Multiple Methods
|
||||
|
||||
More than one method in an API call can be invoked by adding multiple `method` parameters. For example:
|
||||
|
||||
```
|
||||
GET /api/dcim/devices/1/napalm/?method=get_ntp_servers&method=get_ntp_peers
|
||||
|
||||
{
|
||||
"get_ntp_servers": {
|
||||
...
|
||||
},
|
||||
"get_ntp_peers": {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Optional Arguments
|
||||
|
||||
The behavior of NAPALM drivers can be adjusted according to the [optional arguments](https://napalm.readthedocs.io/en/latest/support/index.html#optional-arguments). NetBox exposes those arguments using headers prefixed with `X-NAPALM-`.
|
||||
|
||||
|
||||
For instance, the SSH port is changed to 2222 in this API call:
|
||||
|
||||
```
|
||||
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
|
||||
-H "Authorization: Token f4b378553dacfcfd44c5a0b9ae49b57e29c552b5" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json; indent=4" \
|
||||
-H "X-NAPALM-port: 2222"
|
||||
```
|
||||
17
docs/additional-features/topology-maps.md
Normal file
17
docs/additional-features/topology-maps.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Topology Maps
|
||||
|
||||
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
||||
|
||||
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure).
|
||||
|
||||
To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
|
||||
|
||||
Each line of the **device patterns** field represents a hierarchical layer within the topology map. For example, you might map a traditional network with core, distribution, and access tiers like this:
|
||||
|
||||
```
|
||||
core-switch-[abcd]
|
||||
dist-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.
|
||||
@@ -293,26 +293,6 @@ Session data is used to track authenticated users when they access NetBox. By de
|
||||
|
||||
---
|
||||
|
||||
## STORAGE_BACKEND
|
||||
|
||||
Default: None (local storage)
|
||||
|
||||
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) package, which provides backends for several popular file storage services. If not configured, local filesystem storage will be used.
|
||||
|
||||
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
|
||||
|
||||
---
|
||||
|
||||
## STORAGE_CONFIG
|
||||
|
||||
Default: Empty
|
||||
|
||||
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the [`django-storages` documentation](https://django-storages.readthedocs.io/en/stable/) for more detail.
|
||||
|
||||
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
|
||||
|
||||
---
|
||||
|
||||
## TIME_ZONE
|
||||
|
||||
Default: UTC
|
||||
@@ -321,6 +301,14 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
|
||||
|
||||
---
|
||||
|
||||
## WEBHOOKS_ENABLED
|
||||
|
||||
Default: False
|
||||
|
||||
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../../additional-features/webhooks/) for more information on setup and use.
|
||||
|
||||
---
|
||||
|
||||
## Date and Time Formatting
|
||||
|
||||
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date).
|
||||
|
||||
@@ -25,7 +25,7 @@ NetBox requires access to a PostgreSQL database service to store data. This serv
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
```
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': 'netbox', # PostgreSQL username
|
||||
@@ -42,48 +42,40 @@ DATABASE = {
|
||||
|
||||
[Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of
|
||||
NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching
|
||||
functionality (as well as other planned features). In 2.7, the connection settings were broken down into two sections for
|
||||
webhooks and caching, allowing the user to connect to different Redis instances/databases per feature.
|
||||
functionality (as well as other planned features).
|
||||
|
||||
Redis is configured using a configuration setting similar to `DATABASE` and these settings are the same for both of the `webhooks` and `caching` subsections:
|
||||
Redis is configured using a configuration setting similar to `DATABASE`:
|
||||
|
||||
* `HOST` - Name or IP address of the Redis server (use `localhost` if running locally)
|
||||
* `PORT` - TCP port of the Redis service; leave blank for default port (6379)
|
||||
* `PASSWORD` - Redis password (if set)
|
||||
* `DATABASE` - Numeric database ID
|
||||
* `DATABASE` - Numeric database ID for webhooks
|
||||
* `CACHE_DATABASE` - Numeric database ID for caching
|
||||
* `DEFAULT_TIMEOUT` - Connection timeout in seconds
|
||||
* `SSL` - Use SSL connection to Redis
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
```
|
||||
REDIS = {
|
||||
'webhooks': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'PASSWORD': 'foobar',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'CACHE_DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
```
|
||||
|
||||
!!! note:
|
||||
If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have
|
||||
changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary
|
||||
If you were using these settings in a prior release with webhooks, the `DATABASE` setting remains the same but
|
||||
an additional `CACHE_DATABASE` setting has been added with a default value of 1 to support the caching backend. The
|
||||
`DATABASE` setting will be renamed in a future release of NetBox to better relay the meaning of the setting.
|
||||
|
||||
!!! warning:
|
||||
It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the
|
||||
same Redis instance for both may result in webhook processing data being lost during cache flushing events.
|
||||
It is highly recommended to keep the webhook and cache databases seperate. Using the same database number for both may result in webhook
|
||||
processing data being lost in cache flushing events.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -69,6 +69,14 @@ If the new field will be included in the object list view, add a column to the m
|
||||
|
||||
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
|
||||
|
||||
### 11. Adjust API and model tests
|
||||
### 11. Create/extend test cases
|
||||
|
||||
Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields.
|
||||
Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including:
|
||||
|
||||
* API serializer/view tests
|
||||
* Filter tests
|
||||
* Form tests
|
||||
* Model tests
|
||||
* View tests
|
||||
|
||||
Be diligent to ensure all of the relevant test suites are adapted or extended as necessary to test any new functionality.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
# What is NetBox?
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ NetBox requires a PostgreSQL database to store data. This can be hosted locally
|
||||
The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. 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.
|
||||
|
||||
!!! warning
|
||||
NetBox requires PostgreSQL 9.4 or higher.
|
||||
NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
|
||||
|
||||
# Installation
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@ This section of the documentation discusses installing and configuring the NetBo
|
||||
**Ubuntu**
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev redis-server zlib1g-dev
|
||||
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev redis-server zlib1g-dev
|
||||
```
|
||||
|
||||
**CentOS**
|
||||
|
||||
```no-highlight
|
||||
# yum install -y epel-release
|
||||
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config redis
|
||||
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config redis
|
||||
# easy_install-3.6 pip
|
||||
# ln -s /usr/bin/python36 /usr/bin/python3
|
||||
# ln -s /usr/bin/python3.6 /usr/bin/python3
|
||||
```
|
||||
|
||||
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
|
||||
@@ -90,14 +90,6 @@ NetBox supports integration with the [NAPALM automation](https://napalm-automati
|
||||
# pip3 install napalm
|
||||
```
|
||||
|
||||
## Remote File Storage (Optional)
|
||||
|
||||
By default, NetBox will use the local filesystem to storage uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired backend](../../configuration/optional-settings/#storage_backend) in `configuration.py`.
|
||||
|
||||
```no-highlight
|
||||
# pip3 install django-storages
|
||||
```
|
||||
|
||||
# Configuration
|
||||
|
||||
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
|
||||
@@ -147,22 +139,13 @@ Redis is a in-memory key-value store required as part of the NetBox installation
|
||||
|
||||
```python
|
||||
REDIS = {
|
||||
'webhooks': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'PASSWORD': 'foobar',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'CACHE_DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -212,7 +195,27 @@ Superuser created successfully.
|
||||
```no-highlight
|
||||
# python3 manage.py collectstatic --no-input
|
||||
|
||||
959 static files copied to '/opt/netbox/netbox/static'.
|
||||
You have requested to collect static files at the destination
|
||||
location as specified in your settings:
|
||||
|
||||
/opt/netbox/netbox/static
|
||||
|
||||
This will overwrite existing files!
|
||||
Are you sure you want to do this?
|
||||
|
||||
Type 'yes' to continue, or 'no' to cancel: yes
|
||||
```
|
||||
|
||||
# Load Initial Data (Optional)
|
||||
|
||||
NetBox ships with some initial data to help you get started: RIR definitions, common devices roles, etc. You can delete any seed data that you don't want to keep.
|
||||
|
||||
!!! note
|
||||
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
|
||||
|
||||
```no-highlight
|
||||
# python3 manage.py loaddata initial_data
|
||||
Installed 43 object(s) from 4 fixture(s)
|
||||
```
|
||||
|
||||
# Test the Application
|
||||
@@ -234,11 +237,3 @@ Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on
|
||||
|
||||
!!! warning
|
||||
If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected.
|
||||
|
||||
Note that the initial UI will be locked down for non-authenticated users.
|
||||
|
||||

|
||||
|
||||
After logging in as the superuser you created earlier, all areas of the UI will be available.
|
||||
|
||||

|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 use systemd to enable service persistence.
|
||||
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
|
||||
For the sake of brevity, only Ubuntu 18.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.
|
||||
@@ -107,53 +107,47 @@ Install gunicorn:
|
||||
# pip3 install gunicorn
|
||||
```
|
||||
|
||||
Copy `contrib/gunicorn.conf` to `/opt/netbox/gunicorn.conf`. We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade.
|
||||
Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`. More info on `max_requests` can be found in the [gunicorn docs](https://docs.gunicorn.org/en/stable/settings.html#max-requests).
|
||||
|
||||
```no-highlight
|
||||
# cp contrib/gunicorn.py /opt/netbox/gunicorn.py
|
||||
command = '/usr/bin/gunicorn'
|
||||
pythonpath = '/opt/netbox/netbox'
|
||||
bind = '127.0.0.1:8001'
|
||||
workers = 3
|
||||
user = 'www-data'
|
||||
max_requests = 5000
|
||||
max_requests_jitter = 500
|
||||
```
|
||||
|
||||
You may wish to edit this file to change the bound IP address or port number, or to make performance-related adjustments.
|
||||
# supervisord Installation
|
||||
|
||||
# systemd configuration
|
||||
|
||||
We'll use systemd to control the daemonization of NetBox services. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory:
|
||||
Install supervisor:
|
||||
|
||||
```no-highlight
|
||||
# cp contrib/*.service /etc/systemd/system/
|
||||
# apt-get install -y supervisor
|
||||
```
|
||||
|
||||
!!! note
|
||||
These service files assume that gunicorn is installed at `/usr/local/bin/gunicorn`. If the output of `which gunicorn` indicates a different path, you'll need to correct the `ExecStart` path in both files.
|
||||
|
||||
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
|
||||
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
|
||||
|
||||
```no-highlight
|
||||
# systemctl daemon-reload
|
||||
# systemctl start netbox.service
|
||||
# systemctl start netbox-rq.service
|
||||
# systemctl enable netbox.service
|
||||
# systemctl enable netbox-rq.service
|
||||
[program:netbox]
|
||||
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
|
||||
directory = /opt/netbox/netbox/
|
||||
user = www-data
|
||||
|
||||
[program:netbox-rqworker]
|
||||
command = python3 /opt/netbox/netbox/manage.py rqworker
|
||||
directory = /opt/netbox/netbox/
|
||||
user = www-data
|
||||
```
|
||||
|
||||
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
|
||||
Then, restart the supervisor service to detect and run the gunicorn service:
|
||||
|
||||
```
|
||||
# systemctl status netbox.service
|
||||
● netbox.service - NetBox WSGI Service
|
||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Thu 2019-12-12 19:23:40 UTC; 25s ago
|
||||
Docs: https://netbox.readthedocs.io/en/stable/
|
||||
Main PID: 11993 (gunicorn)
|
||||
Tasks: 6 (limit: 2362)
|
||||
CGroup: /system.slice/netbox.service
|
||||
├─11993 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
||||
├─12015 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
||||
├─12016 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
||||
...
|
||||
```no-highlight
|
||||
# service supervisor restart
|
||||
```
|
||||
|
||||
At this point, you should be able to connect to the HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running.
|
||||
At this point, you should be able to connect to the nginx HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running.
|
||||
|
||||
!!! info
|
||||
Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You may want to make adjustments to better suit your production environment.
|
||||
Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment.
|
||||
|
||||
@@ -80,6 +80,7 @@ AUTH_LDAP_USER_ATTR_MAP = {
|
||||
```
|
||||
|
||||
# User Groups for Permissions
|
||||
|
||||
!!! info
|
||||
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. You will also need to modify the import line to use `NestedGroupOfNamesType` instead of `GroupOfNamesType` .
|
||||
|
||||
@@ -117,6 +118,9 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
|
||||
* `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.
|
||||
|
||||
!!! warning
|
||||
Authentication will fail if the groups (the distinguished names) do not exist in the LDAP directory.
|
||||
|
||||
# Troubleshooting LDAP
|
||||
|
||||
`supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`.
|
||||
|
||||
@@ -12,5 +12,3 @@ The following sections detail how to set up a new instance of NetBox:
|
||||
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
|
||||
|
||||
NetBox v2.5 and later requires Python 3.5 or higher. Please see the instructions for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.
|
||||
|
||||
Netbox v2.5.9 and later moved to using systemd instead of supervisord. Please see the instructions for [migrating to systemd](migrating-to-systemd.md) if you are still using supervisord.
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# Migration
|
||||
|
||||
Migration is not required, as supervisord will still continue to function.
|
||||
|
||||
## Ubuntu
|
||||
|
||||
### Remove supervisord:
|
||||
|
||||
```no-highlight
|
||||
# apt-get remove -y supervisord
|
||||
```
|
||||
|
||||
### systemd configuration:
|
||||
|
||||
Copy or link contrib/netbox.service and contrib/netbox-rq.service to /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service
|
||||
|
||||
```no-highlight
|
||||
# cp contrib/netbox.service /etc/systemd/system/netbox.service
|
||||
# cp contrib/netbox-rq.service /etc/systemd/system/netbox-rq.service
|
||||
```
|
||||
|
||||
Edit /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service. Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`). If using CentOS/RHEL. Change the username from `www-data` to `nginx` or `apache`:
|
||||
|
||||
```no-highlight
|
||||
/usr/local/bin/gunicorn --pid ${PidPath} --pythonpath ${WorkingDirectory}/netbox --config ${ConfigPath} netbox.wsgi
|
||||
```
|
||||
|
||||
```no-highlight
|
||||
User=www-data
|
||||
Group=www-data
|
||||
```
|
||||
|
||||
Copy contrib/netbox.env to /etc/sysconfig/netbox.env
|
||||
|
||||
```no-highlight
|
||||
# cp contrib/netbox.env /etc/sysconfig/netbox.env
|
||||
```
|
||||
|
||||
Edit /etc/sysconfig/netbox.env and change the settings as required. Update the `WorkingDirectory` variable if needed.
|
||||
|
||||
```no-highlight
|
||||
# Name is the Process Name
|
||||
#
|
||||
Name = 'Netbox'
|
||||
|
||||
# ConfigPath is the path to the gunicorn config file.
|
||||
#
|
||||
ConfigPath=/opt/netbox/gunicorn.conf
|
||||
|
||||
# WorkingDirectory is the Working Directory for Netbox.
|
||||
#
|
||||
WorkingDirectory=/opt/netbox/
|
||||
|
||||
# PidPath is the path to the pid for the netbox WSGI
|
||||
#
|
||||
PidPath=/var/run/netbox.pid
|
||||
```
|
||||
|
||||
Copy contrib/gunicorn.conf to gunicorn.conf
|
||||
|
||||
```no-highlight
|
||||
# cp contrib/gunicorn.conf to gunicorn.conf
|
||||
```
|
||||
|
||||
Edit gunicorn.conf and change the settings as required.
|
||||
|
||||
```
|
||||
# Bind is the ip and port that the Netbox WSGI should bind to
|
||||
#
|
||||
bind='127.0.0.1:8001'
|
||||
|
||||
# Workers is the number of workers that GUnicorn should spawn.
|
||||
# Workers should be: cores * 2 + 1. So if you have 8 cores, it would be 17.
|
||||
#
|
||||
workers=3
|
||||
|
||||
# Threads
|
||||
# The number of threads for handling requests
|
||||
#
|
||||
threads=3
|
||||
|
||||
# Timeout is the timeout between gunicorn receiving a request and returning a response (or failing with a 500 error)
|
||||
#
|
||||
timeout=120
|
||||
|
||||
# ErrorLog
|
||||
# ErrorLog is the logfile for the ErrorLog
|
||||
#
|
||||
errorlog='/opt/netbox/netbox.log'
|
||||
```
|
||||
|
||||
Finally, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
|
||||
|
||||
```no-highlight
|
||||
# systemctl daemon-reload
|
||||
# systemctl start netbox.service
|
||||
# systemctl start netbox-rq.service
|
||||
# systemctl enable netbox.service
|
||||
# systemctl enable netbox-rq.service
|
||||
```
|
||||
@@ -84,12 +84,14 @@ This script:
|
||||
|
||||
# Restart the WSGI Service
|
||||
|
||||
Finally, restart the WSGI services to run the new code. If you followed this guide for the initial installation, this is done using `systemctl:
|
||||
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox
|
||||
# sudo systemctl restart netbox-rqworker
|
||||
# sudo supervisorctl restart netbox
|
||||
```
|
||||
|
||||
!!! note
|
||||
It's possible you are still using supervisord instead of the linux native systemd. If you are still using supervisord you can restart the services by either restarting supervisord or by using supervisorctl to restart netbox.
|
||||
If using webhooks, also restart the Redis worker:
|
||||
|
||||
```no-highlight
|
||||
# sudo supervisorctl restart netbox-rqworker
|
||||
```
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
21
docs/netbox_logo.svg
Normal file
21
docs/netbox_logo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 320">
|
||||
<g fill="#9cc8f8" stroke="#9cc8f8">
|
||||
<circle cx="37" cy="284" r="23"/>
|
||||
<circle cx="101" cy="37" r="23"/>
|
||||
<circle cx="101" cy="220" r="23"/>
|
||||
<circle cx="284" cy="220" r="23"/>
|
||||
<rect x="93" y="37" width="16" height="180"/>
|
||||
<rect x="101" y="212" width="180" height="16"/>
|
||||
<rect x="93" y="212" width="16" height="90" transform="rotate(45 101 220)"/>
|
||||
</g>
|
||||
<g fill="#1685fc" stroke="#1685fc">
|
||||
<circle cx="284" cy="37" r="23"/>
|
||||
<circle cx="37" cy="101" r="23"/>
|
||||
<circle cx="220" cy="101" r="23"/>
|
||||
<circle cx="220" cy="284" r="23"/>
|
||||
<rect x="37" y="93" width="180" height="16"/>
|
||||
<rect x="212" y="101" width="16" height="180"/>
|
||||
<rect x="212" y="93" width="16" height="90" transform="rotate(225 220 101)"/>
|
||||
<path transform="translate(380, 8)" d="M13.60 200L13.60 104L36.40 104L36.40 119.40L36.80 119.40Q40.20 112.20 47.20 106.90Q54.20 101.60 66.20 101.60L66.20 101.60Q75.80 101.60 82.50 104.80Q89.20 108 93.40 113.20Q97.60 118.40 99.40 125.20Q101.20 132 101.20 139.40L101.20 139.40L101.20 200L77.20 200L77.20 151.40Q77.20 147.40 76.80 142.50Q76.40 137.60 74.70 133.30Q73 129 69.40 126.10Q65.80 123.20 59.60 123.20L59.60 123.20Q53.60 123.20 49.50 125.20Q45.40 127.20 42.70 130.60Q40 134 38.80 138.40Q37.60 142.80 37.60 147.60L37.60 147.60L37.60 200L13.60 200ZM224.80 160.40L151.60 160.40Q152.80 171.20 160 177.20Q167.20 183.20 177.40 183.20L177.40 183.20Q186.40 183.20 192.50 179.50Q198.60 175.80 203.20 170.20L203.20 170.20L220.40 183.20Q212 193.60 201.60 198Q191.20 202.40 179.80 202.40L179.80 202.40Q169 202.40 159.40 198.80Q149.80 195.20 142.80 188.60Q135.80 182 131.70 172.70Q127.60 163.40 127.60 152L127.60 152Q127.60 140.60 131.70 131.30Q135.80 122 142.80 115.40Q149.80 108.80 159.40 105.20Q169 101.60 179.80 101.60L179.80 101.60Q189.80 101.60 198.10 105.10Q206.40 108.60 212.30 115.20Q218.20 121.80 221.50 131.50Q224.80 141.20 224.80 153.80L224.80 153.80L224.80 160.40ZM151.60 142.40L200.80 142.40Q200.60 131.80 194.20 125.70Q187.80 119.60 176.40 119.60L176.40 119.60Q165.60 119.60 159.30 125.80Q153 132 151.60 142.40L151.60 142.40ZM259.80 124.40L240.00 124.40L240.00 104L259.80 104L259.80 76.20L283.80 76.20L283.80 104L310.20 104L310.20 124.40L283.80 124.40L283.80 166.40Q283.80 173.60 286.50 177.80Q289.20 182 297.20 182L297.20 182Q300.40 182 304.20 181.30Q308 180.60 310.20 179L310.20 179L310.20 199.20Q306.40 201 300.90 201.70Q295.40 202.40 291.20 202.40L291.20 202.40Q281.60 202.40 275.50 200.30Q269.40 198.20 265.90 193.90Q262.40 189.60 261.10 183.20Q259.80 176.80 259.80 168.40L259.80 168.40L259.80 124.40ZM333.20 200L333.20 48.80L357.20 48.80L357.20 116.20L357.80 116.20Q359.60 113.80 362.40 111.30Q365.20 108.80 369.20 106.60Q373.20 104.40 378.40 103Q383.60 101.60 390.40 101.60L390.40 101.60Q400.60 101.60 409.20 105.50Q417.80 109.40 423.90 116.20Q430 123 433.40 132.20Q436.80 141.40 436.80 152L436.80 152Q436.80 162.60 433.60 171.80Q430.40 181 424.20 187.80Q418 194.60 409.20 198.50Q400.40 202.40 389.40 202.40L389.40 202.40Q379.20 202.40 370.40 198.40Q361.60 194.40 356.40 185.60L356.40 185.60L356 185.60L356 200L333.20 200ZM412.80 152L412.80 152Q412.80 146.40 410.90 141.20Q409 136 405.30 132Q401.60 128 396.40 125.60Q391.20 123.20 384.60 123.20L384.60 123.20Q378 123.20 372.80 125.60Q367.60 128 363.90 132Q360.20 136 358.30 141.20Q356.40 146.40 356.40 152L356.40 152Q356.40 157.60 358.30 162.80Q360.20 168 363.90 172Q367.60 176 372.80 178.40Q378 180.80 384.60 180.80L384.60 180.80Q391.20 180.80 396.40 178.40Q401.60 176 405.30 172Q409 168 410.90 162.80Q412.80 157.60 412.80 152ZM458.40 152L458.40 152Q458.40 140.60 462.50 131.30Q466.60 122 473.60 115.40Q480.60 108.80 490.20 105.20Q499.80 101.60 510.60 101.60L510.60 101.60Q521.40 101.60 531 105.20Q540.60 108.80 547.60 115.40Q554.60 122 558.70 131.30Q562.80 140.60 562.80 152L562.80 152Q562.80 163.40 558.70 172.70Q554.60 182 547.60 188.60Q540.60 195.20 531 198.80Q521.40 202.40 510.60 202.40L510.60 202.40Q499.80 202.40 490.20 198.80Q480.60 195.20 473.60 188.60Q466.60 182 462.50 172.70Q458.40 163.40 458.40 152ZM482.40 152L482.40 152Q482.40 157.60 484.30 162.80Q486.20 168 489.90 172Q493.60 176 498.80 178.40Q504 180.80 510.60 180.80L510.60 180.80Q517.20 180.80 522.40 178.40Q527.60 176 531.30 172Q535 168 536.90 162.80Q538.80 157.60 538.80 152L538.80 152Q538.80 146.40 536.90 141.20Q535 136 531.30 132Q527.60 128 522.40 125.60Q517.20 123.20 510.60 123.20L510.60 123.20Q504 123.20 498.80 125.60Q493.60 128 489.90 132Q486.20 136 484.30 141.20Q482.40 146.40 482.40 152ZM575.40 200L614 148.40L580.80 104L610 104L629.20 132.80L650 104L677.40 104L644.60 148.40L683.20 200L654 200L629 165.60L603.80 200L575.40 200Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -1 +1 @@
|
||||
version-2.7.md
|
||||
version-2.6.md
|
||||
@@ -1,3 +1,92 @@
|
||||
# v2.6.12 (2020-01-13)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger (OpenAPI)
|
||||
* [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering over the link
|
||||
* [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers
|
||||
* [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle the display of child prefixes/IP addresses
|
||||
* [#3009](https://github.com/netbox-community/netbox/issues/3009) - Search by description when assigning IP address to interfaces
|
||||
* [#3021](https://github.com/netbox-community/netbox/issues/3021) - Add `tenant` filter field for cables
|
||||
* [#3090](https://github.com/netbox-community/netbox/issues/3090) - Enable filtering of interfaces by name on the device view
|
||||
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations view
|
||||
* [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate assigned circuits at the provider details view
|
||||
* [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total path length to cable trace
|
||||
* [#3491](https://github.com/netbox-community/netbox/issues/3491) - Include content of response on webhook error
|
||||
* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Enable word expansion during interface creation
|
||||
* [#3668](https://github.com/netbox-community/netbox/issues/3668) - Enable searching by DNS name when assigning IP address
|
||||
* [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms
|
||||
* [#3891](https://github.com/netbox-community/netbox/issues/3891) - Add `local_context_data` filter for virtual machines
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#3589](https://github.com/netbox-community/netbox/issues/3589) - Fix validation on tagged VLANs of an interface
|
||||
* [#3849](https://github.com/netbox-community/netbox/issues/3849) - Fix ordering of models when dumping data to JSON
|
||||
* [#3853](https://github.com/netbox-community/netbox/issues/3853) - Fix device role link on config context view
|
||||
* [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses
|
||||
* [#3857](https://github.com/netbox-community/netbox/issues/3857) - Fix rendering of grouped custom links
|
||||
* [#3862](https://github.com/netbox-community/netbox/issues/3862) - Allow filtering device components by multiple device names
|
||||
* [#3864](https://github.com/netbox-community/netbox/issues/3864) - Disallow /0 masks for prefixes and IP addresses
|
||||
* [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs on the IP address view
|
||||
* [#3876](https://github.com/netbox-community/netbox/issues/3876) - Fix minimum/maximum value rendering for site ASN field
|
||||
* [#3882](https://github.com/netbox-community/netbox/issues/3882) - Fix filtering of devices by rack group
|
||||
* [#3898](https://github.com/netbox-community/netbox/issues/3898) - Fix references to deleted cables without a label
|
||||
* [#3905](https://github.com/netbox-community/netbox/issues/3905) - Fix divide-by-zero on power feeds with low power values
|
||||
|
||||
---
|
||||
|
||||
# v2.6.11 (2020-01-03)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#3831](https://github.com/netbox-community/netbox/issues/3831) - Fix API-driven filter field rendering (#3812 regression)
|
||||
* [#3833](https://github.com/netbox-community/netbox/issues/3833) - Add missing region filters for multiple objects
|
||||
|
||||
---
|
||||
|
||||
# v2.6.10 (2020-01-02)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2233](https://github.com/netbox-community/netbox/issues/2233) - Add ability to move inventory items between devices
|
||||
* [#2892](https://github.com/netbox-community/netbox/issues/2892) - Extend admin UI to allow deleting old report results
|
||||
* [#3062](https://github.com/netbox-community/netbox/issues/3062) - Add `assigned_to_interface` filter for IP addresses
|
||||
* [#3461](https://github.com/netbox-community/netbox/issues/3461) - Fail gracefully on custom link rendering exception
|
||||
* [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts
|
||||
* [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets
|
||||
* [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items
|
||||
* [#3812](https://github.com/netbox-community/netbox/issues/3812) - Optimize size of pages containing a dynamic selection field
|
||||
* [#3827](https://github.com/netbox-community/netbox/issues/3827) - Allow filtering console/power/interface connections by device ID
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#3106](https://github.com/netbox-community/netbox/issues/3106) - Restrict queryset of chained fields when form validation fails
|
||||
* [#3695](https://github.com/netbox-community/netbox/issues/3695) - Include A/Z termination sites for circuits in global search
|
||||
* [#3712](https://github.com/netbox-community/netbox/issues/3712) - Scrolling to target (hash) did not account for the header size
|
||||
* [#3780](https://github.com/netbox-community/netbox/issues/3780) - Fix AttributeError exception in API docs
|
||||
* [#3809](https://github.com/netbox-community/netbox/issues/3809) - Filter platform by manufacturer when editing devices
|
||||
* [#3811](https://github.com/netbox-community/netbox/issues/3811) - Fix filtering of racks by group on device list
|
||||
* [#3822](https://github.com/netbox-community/netbox/issues/3822) - Fix exception when editing a device bay (regression from #3596)
|
||||
|
||||
---
|
||||
|
||||
# v2.6.9 (2019-12-16)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#3152](https://github.com/netbox-community/netbox/issues/3152) - Include direct link to rack elevations on site view
|
||||
* [#3441](https://github.com/netbox-community/netbox/issues/3441) - Move virtual machine results near devices in global search
|
||||
* [#3761](https://github.com/netbox-community/netbox/issues/3761) - Added copy button for API tokens
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2170](https://github.com/netbox-community/netbox/issues/2170) - Prevent the deletion of a virtual chassis when a cross-member LAG is present
|
||||
* [#2358](https://github.com/netbox-community/netbox/issues/2358) - Respect custom field default values when creating objects via the REST API
|
||||
* [#3749](https://github.com/netbox-community/netbox/issues/3749) - Fix exception on password change page for local users
|
||||
* [#3757](https://github.com/netbox-community/netbox/issues/3757) - Fix unable to assign IP to interface
|
||||
|
||||
---
|
||||
|
||||
# v2.6.8 (2019-12-10)
|
||||
|
||||
## Enhancements
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
# v2.7.0 (FUTURE)
|
||||
|
||||
**Note:** NetBox v2.7 is the last major release that will support Python 3.5. Beginning with NetBox v2.8, Python 3.6 or
|
||||
higher will be required.
|
||||
|
||||
## New Features
|
||||
|
||||
### Enhanced Device Type Import ([#451](https://github.com/netbox-community/netbox/issues/451))
|
||||
|
||||
NetBox now supports the import of device types and related component templates using a definition written in YAML or
|
||||
JSON. For example, the following will create a new device type with four network interfaces, two power ports, and a
|
||||
console port:
|
||||
|
||||
```yaml
|
||||
manufacturer: Acme
|
||||
model: Packet Shooter 9000
|
||||
slug: packet-shooter-9000
|
||||
u_height: 1
|
||||
interfaces:
|
||||
- name: ge-0/0/0
|
||||
type: 1000base-t
|
||||
- name: ge-0/0/1
|
||||
type: 1000base-t
|
||||
- name: ge-0/0/2
|
||||
type: 1000base-t
|
||||
- name: ge-0/0/3
|
||||
type: 1000base-t
|
||||
power-ports:
|
||||
- name: PSU0
|
||||
- name: PSU1
|
||||
console-ports:
|
||||
- name: Console
|
||||
```
|
||||
|
||||
This new functionality replaces the existing CSV-based import form, which did not allow for component template import.
|
||||
|
||||
### Bulk Import of Device Components ([#822](https://github.com/netbox-community/netbox/issues/822))
|
||||
|
||||
NetBox now supports the bulk import of device components such as console ports, power ports, and interfaces across
|
||||
multiple devices. Device components can be imported in CSV-format.
|
||||
|
||||
Here's an example bulk import of interfaces to several devices:
|
||||
|
||||
```
|
||||
device,name,type
|
||||
Switch1,Vlan100,Virtual
|
||||
Switch1,Vlan200,Virtual
|
||||
Switch2,Vlan100,Virtual
|
||||
Switch2,Vlan200,Virtual
|
||||
```
|
||||
|
||||
### External File Storage ([#1814](https://github.com/netbox-community/netbox/issues/1814))
|
||||
|
||||
In prior releases, the only option for storing uploaded files (e.g. image attachments) was to save them to the local
|
||||
filesystem on the NetBox server. This release introduces support for several remote storage backends provided by the
|
||||
[`django-storages`](https://django-storages.readthedocs.io/en/stable/) library. These include:
|
||||
|
||||
* Amazon S3
|
||||
* ApacheLibcloud
|
||||
* Azure Storage
|
||||
* DigitalOcean Spaces
|
||||
* Dropbox
|
||||
* FTP
|
||||
* Google Cloud Storage
|
||||
* SFTP
|
||||
|
||||
To enable remote file storage, first install `django-storages`:
|
||||
|
||||
```
|
||||
pip install django-storages
|
||||
```
|
||||
|
||||
Then, set the appropriate storage backend and its configuration in `configuration.py`. Here's an example using Amazon
|
||||
S3:
|
||||
|
||||
```python
|
||||
STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'
|
||||
STORAGE_CONFIG = {
|
||||
'AWS_ACCESS_KEY_ID': '<Key>',
|
||||
'AWS_SECRET_ACCESS_KEY': '<Secret>',
|
||||
'AWS_STORAGE_BUCKET_NAME': 'netbox',
|
||||
'AWS_S3_REGION_NAME': 'eu-west-1',
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to [@steffann](https://github.com/steffann) for contributing this work!
|
||||
|
||||
## Changes
|
||||
|
||||
### Rack Elevations Rendered via SVG ([#2248](https://github.com/netbox-community/netbox/issues/2248))
|
||||
|
||||
NetBox v2.7 introduces a new method of rendering rack elevations as an
|
||||
[SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) via a REST API endpoint. This replaces the prior method of
|
||||
rendering elevations using pure HTML which was cumbersome and had several shortcomings. Allowing elevations to be
|
||||
rendered as an SVG image in the API allows users to retrieve and make use of the drawings in their own tooling. This
|
||||
also opens the door to other feature requests related to rack elevations in the NetBox backlog.
|
||||
|
||||
This feature implements a new REST API endpoint:
|
||||
|
||||
```
|
||||
/api/dcim/racks/<id>/elevation/
|
||||
```
|
||||
|
||||
By default, this endpoint returns a paginated JSON response representing each rack unit in the given elevation. This is
|
||||
the same response returned by the rack units detail endpoint and for this reason the rack units endpoint has been
|
||||
deprecated and will be removed in v2.8 (see [#3753](https://github.com/netbox-community/netbox/issues/3753)):
|
||||
|
||||
```
|
||||
/api/dcim/racks/<id>/units/
|
||||
```
|
||||
|
||||
In order to render the elevation as an SVG, include the `render=svg` query parameter in the request. You may also
|
||||
control the width of the elevation drawing in pixels with `unit_width=<width in pixels>` and the height of each rack
|
||||
unit with `unit_height=<height in pixels>`. The `unit_width` defaults to `230` and the `unit_height` default to `20`
|
||||
which produces elevations the same size as those that appear in the NetBox Web UI. The query parameter `face` is used to
|
||||
request either the `front` or `rear` of the elevation and defaults to `front`.
|
||||
|
||||
Here is an example of the request url for an SVG rendering using the default parameters to render the front of the
|
||||
elevation:
|
||||
|
||||
```
|
||||
/api/dcim/racks/<id>/elevation/?render=svg
|
||||
```
|
||||
|
||||
Here is an example of the request url for an SVG rendering of the rear of the elevation having a width of 300 pixels and
|
||||
per unit height of 35 pixels:
|
||||
|
||||
```
|
||||
/api/dcim/racks/<id>/elevation/?render=svg&face=rear&unit_width=300&unit_height=35
|
||||
```
|
||||
|
||||
Thanks to [@hellerve](https://github.com/hellerve) for doing the heavy lifting on this!
|
||||
|
||||
### Topology Maps Removed ([#2745](https://github.com/netbox-community/netbox/issues/2745))
|
||||
|
||||
The topology maps feature has been removed to help focus NetBox development efforts.
|
||||
|
||||
### Redis Configuration ([#3282](https://github.com/netbox-community/netbox/issues/3282))
|
||||
|
||||
v2.6.0 introduced caching and added the `CACHE_DATABASE` option to the existing `REDIS` database configuration section.
|
||||
This did not however, allow for using two different Redis connections for the seperate caching and webhooks features.
|
||||
This change separates the Redis connection configurations in the `REDIS` section into distinct `webhooks` and `caching`
|
||||
subsections. This requires modification of the `REDIS` section of the `configuration.py` file as follows:
|
||||
|
||||
Old Redis configuration:
|
||||
```python
|
||||
REDIS = {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'CACHE_DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
```
|
||||
|
||||
New Redis configuration:
|
||||
```python
|
||||
REDIS = {
|
||||
'webhooks': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'PASSWORD': 'foobar',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that `CACHE_DATABASE` has been removed and the connection settings have been duplicated for both `webhooks` and
|
||||
`caching`. This allows the user to make use of separate Redis instances and/or databases if desired. Full connection
|
||||
details are required in both sections, even if they are the same.
|
||||
|
||||
### WEBHOOKS_ENABLED Configuration Setting Removed ([#3408](https://github.com/netbox-community/netbox/issues/3408))
|
||||
|
||||
As `django-rq` is now a required library, NetBox assumes that the RQ worker process is running. The installation and
|
||||
upgrade documentation has been updated to reflect this, and the `WEBHOOKS_ENABLED` configuration parameter is no longer
|
||||
used. Please ensure that both the NetBox WSGI service and the RQ worker process are running on all production
|
||||
installations.
|
||||
|
||||
### API Choice Fields Now Use String Values ([#3569](https://github.com/netbox-community/netbox/issues/3569))
|
||||
|
||||
NetBox's REST API presents fields which reference a particular choice as a dictionary with two keys: `value` and
|
||||
`label`. In previous versions, `value` was an integer which represented the particular choice in the database. This has
|
||||
been changed to a more human-friendly "slug" string, which is essentially a simplified version of the choice's `label`.
|
||||
|
||||
For example, The site status field was previously represented as:
|
||||
|
||||
```json
|
||||
"status": {
|
||||
"value": 1,
|
||||
"label": "Active"
|
||||
},
|
||||
```
|
||||
|
||||
Beginning with v2.7.0, it now looks like this:
|
||||
|
||||
```json
|
||||
"status": {
|
||||
"value": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
```
|
||||
|
||||
This change allows for much more intuitive representation of values, and obviates the need for API consumers to maintain
|
||||
a mapping of static integer values.
|
||||
|
||||
Note that that all v2.7 releases will continue to accept the legacy integer values in write requests (POST, PUT, and
|
||||
PATCH) to maintain backward compatibility. This behavior will be discontinued beginning in v2.8.0.
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#33](https://github.com/digitalocean/netbox/issues/33) - Add ability to clone objects (pre-populate form fields)
|
||||
* [#648](https://github.com/digitalocean/netbox/issues/648) - Pre-populate forms when selecting "create and add another"
|
||||
* [#792](https://github.com/digitalocean/netbox/issues/792) - Add power port and power outlet types
|
||||
* [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types
|
||||
* [#2669](https://github.com/digitalocean/netbox/issues/2669) - Relax uniqueness constraint on device and VM names
|
||||
* [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace `supervisord` with `systemd`
|
||||
* [#3455](https://github.com/digitalocean/netbox/issues/3455) - Add tenant assignment to cluster
|
||||
* [#3564](https://github.com/digitalocean/netbox/issues/3564) - Add list views for device components
|
||||
* [#3538](https://github.com/digitalocean/netbox/issues/3538) - Introduce a REST API endpoint for executing custom
|
||||
scripts
|
||||
* [#3655](https://github.com/digitalocean/netbox/issues/3655) - Add `description` field to organizational models
|
||||
* [#3664](https://github.com/digitalocean/netbox/issues/3664) - Enable applying configuration contexts by tags
|
||||
* [#3706](https://github.com/digitalocean/netbox/issues/3706) - Increase `available_power` maximum value on PowerFeed
|
||||
* [#3731](https://github.com/digitalocean/netbox/issues/3731) - Change Graph.type to a ContentType foreign key field
|
||||
|
||||
## API Changes
|
||||
|
||||
* Choice fields now use human-friendly strings for their values instead of integers (see
|
||||
[#3569](https://github.com/netbox-community/netbox/issues/3569)).
|
||||
* Introduced `/api/extras/scripts/` endpoint for retrieving and executing custom scripts
|
||||
* circuits.CircuitType: Added field `description`
|
||||
* dcim.ConsolePort: Added field `type`
|
||||
* dcim.ConsolePortTemplate: Added field `type`
|
||||
* dcim.ConsoleServerPort: Added field `type`
|
||||
* dcim.ConsoleServerPortTemplate: Added field `type`
|
||||
* dcim.DeviceRole: Added field `description`
|
||||
* dcim.PowerPort: Added field `type`
|
||||
* dcim.PowerPortTemplate: Added field `type`
|
||||
* dcim.PowerOutlet: Added field `type`
|
||||
* dcim.PowerOutletTemplate: Added field `type`
|
||||
* dcim.RackRole: Added field `description`
|
||||
* extras.Graph: The `type` field has been changed to a content type foreign key. Models are specified as
|
||||
`<app>.<model>`; e.g. `dcim.site`.
|
||||
* ipam.Role: Added field `description`
|
||||
* secrets.SecretRole: Added field `description`
|
||||
* virtualization.Cluster: Added field `tenant`
|
||||
@@ -35,6 +35,7 @@ pages:
|
||||
- Custom Scripts: 'additional-features/custom-scripts.md'
|
||||
- Export Templates: 'additional-features/export-templates.md'
|
||||
- Graphs: 'additional-features/graphs.md'
|
||||
- NAPALM: 'additional-features/napalm.md'
|
||||
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
||||
- Reports: 'additional-features/reports.md'
|
||||
- Tags: 'additional-features/tags.md'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.constants import CIRCUIT_STATUS_CHOICES
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import ConnectedEndpointSerializer
|
||||
@@ -36,12 +36,12 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug', 'description', 'circuit_count']
|
||||
fields = ['id', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
provider = NestedProviderSerializer()
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
|
||||
type = NestedCircuitTypeSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
@@ -7,7 +7,7 @@ from circuits import filters
|
||||
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from utilities.api import FieldChoicesViewSet, ModelViewSet
|
||||
from . import serializers
|
||||
|
||||
@@ -40,7 +40,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
||||
A convenience method for rendering graphs for a particular provider.
|
||||
"""
|
||||
provider = get_object_or_404(Provider, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='provider')
|
||||
queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER)
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
from utilities.choices import ChoiceSet
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitStatusChoices(ChoiceSet):
|
||||
|
||||
STATUS_DEPROVISIONING = 'deprovisioning'
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_PLANNED = 'planned'
|
||||
STATUS_PROVISIONING = 'provisioning'
|
||||
STATUS_OFFLINE = 'offline'
|
||||
STATUS_DECOMMISSIONED = 'decommissioned'
|
||||
|
||||
CHOICES = (
|
||||
(STATUS_PLANNED, 'Planned'),
|
||||
(STATUS_PROVISIONING, 'Provisioning'),
|
||||
(STATUS_ACTIVE, 'Active'),
|
||||
(STATUS_OFFLINE, 'Offline'),
|
||||
(STATUS_DEPROVISIONING, 'Deprovisioning'),
|
||||
(STATUS_DECOMMISSIONED, 'Decommissioned'),
|
||||
)
|
||||
|
||||
LEGACY_MAP = {
|
||||
STATUS_DEPROVISIONING: 0,
|
||||
STATUS_ACTIVE: 1,
|
||||
STATUS_PLANNED: 2,
|
||||
STATUS_PROVISIONING: 3,
|
||||
STATUS_OFFLINE: 4,
|
||||
STATUS_DECOMMISSIONED: 5,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# CircuitTerminations
|
||||
#
|
||||
|
||||
class CircuitTerminationSideChoices(ChoiceSet):
|
||||
|
||||
SIDE_A = 'A'
|
||||
SIDE_Z = 'Z'
|
||||
|
||||
CHOICES = (
|
||||
(SIDE_A, 'A'),
|
||||
(SIDE_Z, 'Z')
|
||||
)
|
||||
23
netbox/circuits/constants.py
Normal file
23
netbox/circuits/constants.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Circuit statuses
|
||||
CIRCUIT_STATUS_DEPROVISIONING = 0
|
||||
CIRCUIT_STATUS_ACTIVE = 1
|
||||
CIRCUIT_STATUS_PLANNED = 2
|
||||
CIRCUIT_STATUS_PROVISIONING = 3
|
||||
CIRCUIT_STATUS_OFFLINE = 4
|
||||
CIRCUIT_STATUS_DECOMMISSIONED = 5
|
||||
CIRCUIT_STATUS_CHOICES = [
|
||||
[CIRCUIT_STATUS_PLANNED, 'Planned'],
|
||||
[CIRCUIT_STATUS_PROVISIONING, 'Provisioning'],
|
||||
[CIRCUIT_STATUS_ACTIVE, 'Active'],
|
||||
[CIRCUIT_STATUS_OFFLINE, 'Offline'],
|
||||
[CIRCUIT_STATUS_DEPROVISIONING, 'Deprovisioning'],
|
||||
[CIRCUIT_STATUS_DECOMMISSIONED, 'Decommissioned'],
|
||||
]
|
||||
|
||||
# CircuitTermination sides
|
||||
TERM_SIDE_A = 'A'
|
||||
TERM_SIDE_Z = 'Z'
|
||||
TERM_SIDE_CHOICES = (
|
||||
(TERM_SIDE_A, 'A'),
|
||||
(TERM_SIDE_Z, 'Z'),
|
||||
)
|
||||
@@ -5,9 +5,16 @@ from dcim.models import Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilter',
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitTypeFilter',
|
||||
'ProviderFilter',
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
@@ -18,6 +25,17 @@ class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
@@ -84,7 +102,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter
|
||||
label='Circuit type (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
choices=CIRCUIT_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
|
||||
26
netbox/circuits/fixtures/initial_data.json
Normal file
26
netbox/circuits/fixtures/initial_data.json
Normal file
@@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"model": "circuits.circuittype",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Internet",
|
||||
"slug": "internet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "circuits.circuittype",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Private WAN",
|
||||
"slug": "private-wan"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "circuits.circuittype",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Out-of-Band",
|
||||
"slug": "out-of-band"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -7,9 +7,9 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
|
||||
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
|
||||
DatePicker, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
|
||||
)
|
||||
from .choices import CircuitStatusChoices
|
||||
from .constants import *
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
|
||||
@@ -104,6 +104,18 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
region = FilterChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/regions/",
|
||||
value_field="slug",
|
||||
filter_for={
|
||||
'site': 'region'
|
||||
}
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
@@ -128,7 +140,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'description',
|
||||
'name', 'slug',
|
||||
]
|
||||
|
||||
|
||||
@@ -161,7 +173,6 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
]
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'install_date': "Format: YYYY-MM-DD",
|
||||
'commit_rate': "Committed rate",
|
||||
}
|
||||
widgets = {
|
||||
@@ -172,7 +183,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
api_url="/api/circuits/circuit-types/"
|
||||
),
|
||||
'status': StaticSelect2(),
|
||||
|
||||
'install_date': DatePicker(),
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +205,7 @@ class CircuitCSVForm(forms.ModelForm):
|
||||
}
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
choices=CIRCUIT_STATUS_CHOICES,
|
||||
required=False,
|
||||
help_text='Operational status'
|
||||
)
|
||||
@@ -235,7 +246,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
)
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(CircuitStatusChoices),
|
||||
choices=add_blank_choice(CIRCUIT_STATUS_CHOICES),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect2()
|
||||
@@ -292,7 +303,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
)
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=CircuitStatusChoices,
|
||||
choices=CIRCUIT_STATUS_CHOICES,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
@@ -303,6 +314,9 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/regions/",
|
||||
value_field="slug",
|
||||
filter_for={
|
||||
'site': 'region'
|
||||
}
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
CIRCUIT_STATUS_CHOICES = (
|
||||
(0, 'deprovisioning'),
|
||||
(1, 'active'),
|
||||
(2, 'planned'),
|
||||
(3, 'provisioning'),
|
||||
(4, 'offline'),
|
||||
(5, 'decommissioned')
|
||||
)
|
||||
|
||||
|
||||
def circuit_status_to_slug(apps, schema_editor):
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
for id, slug in CIRCUIT_STATUS_CHOICES:
|
||||
Circuit.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0015_custom_tag_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# Circuit.status
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=circuit_status_to_slug
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.6 on 2019-12-10 18:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0016_3569_circuit_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -3,13 +3,13 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import CableTermination
|
||||
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
|
||||
|
||||
class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
@@ -57,12 +57,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
|
||||
]
|
||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -98,12 +93,8 @@ class CircuitType(ChangeLoggedModel):
|
||||
slug = models.SlugField(
|
||||
unique=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -118,7 +109,6 @@ class CircuitType(ChangeLoggedModel):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
@@ -142,10 +132,9 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuits'
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=50,
|
||||
choices=CircuitStatusChoices,
|
||||
default=CircuitStatusChoices.STATUS_ACTIVE
|
||||
status = models.PositiveSmallIntegerField(
|
||||
choices=CIRCUIT_STATUS_CHOICES,
|
||||
default=CIRCUIT_STATUS_ACTIVE
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
@@ -181,18 +170,6 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
csv_headers = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
clone_fields = [
|
||||
'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||
]
|
||||
|
||||
STATUS_CLASS_MAP = {
|
||||
CircuitStatusChoices.STATUS_DEPROVISIONING: 'warning',
|
||||
CircuitStatusChoices.STATUS_ACTIVE: 'success',
|
||||
CircuitStatusChoices.STATUS_PLANNED: 'info',
|
||||
CircuitStatusChoices.STATUS_PROVISIONING: 'primary',
|
||||
CircuitStatusChoices.STATUS_OFFLINE: 'danger',
|
||||
CircuitStatusChoices.STATUS_DECOMMISSIONED: 'default',
|
||||
}
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
@@ -218,7 +195,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return self.STATUS_CLASS_MAP.get(self.status)
|
||||
return STATUS_CLASSES[self.status]
|
||||
|
||||
def _get_termination(self, side):
|
||||
for ct in self.terminations.all():
|
||||
@@ -243,7 +220,7 @@ class CircuitTermination(CableTermination):
|
||||
)
|
||||
term_side = models.CharField(
|
||||
max_length=1,
|
||||
choices=CircuitTerminationSideChoices,
|
||||
choices=TERM_SIDE_CHOICES,
|
||||
verbose_name='Termination'
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
|
||||
@@ -50,14 +50,12 @@ class CircuitTypeTable(BaseTable):
|
||||
name = tables.LinkColumn()
|
||||
circuit_count = tables.Column(verbose_name='Circuits')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CIRCUITTYPE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'circuit_count', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.models import Site
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
|
||||
from extras.constants import GRAPH_TYPE_PROVIDER
|
||||
from extras.models import Graph
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
@@ -28,20 +28,16 @@ class ProviderTest(APITestCase):
|
||||
|
||||
def test_get_provider_graphs(self):
|
||||
|
||||
provider_ct = ContentType.objects.get(app_label='circuits', model='provider')
|
||||
self.graph1 = Graph.objects.create(
|
||||
type=provider_ct,
|
||||
name='Test Graph 1',
|
||||
type=GRAPH_TYPE_PROVIDER, name='Test Graph 1',
|
||||
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'
|
||||
)
|
||||
self.graph2 = Graph.objects.create(
|
||||
type=provider_ct,
|
||||
name='Test Graph 2',
|
||||
type=GRAPH_TYPE_PROVIDER, name='Test Graph 2',
|
||||
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'
|
||||
)
|
||||
self.graph3 = Graph.objects.create(
|
||||
type=provider_ct,
|
||||
name='Test Graph 3',
|
||||
type=GRAPH_TYPE_PROVIDER, name='Test Graph 3',
|
||||
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'
|
||||
)
|
||||
|
||||
@@ -254,7 +250,7 @@ class CircuitTest(APITestCase):
|
||||
'cid': 'TEST0004',
|
||||
'provider': self.provider1.pk,
|
||||
'type': self.circuittype1.pk,
|
||||
'status': CircuitStatusChoices.STATUS_ACTIVE,
|
||||
'status': CIRCUIT_STATUS_ACTIVE,
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:circuit-list')
|
||||
@@ -274,19 +270,19 @@ class CircuitTest(APITestCase):
|
||||
'cid': 'TEST0004',
|
||||
'provider': self.provider1.pk,
|
||||
'type': self.circuittype1.pk,
|
||||
'status': CircuitStatusChoices.STATUS_ACTIVE,
|
||||
'status': CIRCUIT_STATUS_ACTIVE,
|
||||
},
|
||||
{
|
||||
'cid': 'TEST0005',
|
||||
'provider': self.provider1.pk,
|
||||
'type': self.circuittype1.pk,
|
||||
'status': CircuitStatusChoices.STATUS_ACTIVE,
|
||||
'status': CIRCUIT_STATUS_ACTIVE,
|
||||
},
|
||||
{
|
||||
'cid': 'TEST0006',
|
||||
'provider': self.provider1.pk,
|
||||
'type': self.circuittype1.pk,
|
||||
'status': CircuitStatusChoices.STATUS_ACTIVE,
|
||||
'status': CIRCUIT_STATUS_ACTIVE,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -340,28 +336,16 @@ class CircuitTerminationTest(APITestCase):
|
||||
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
|
||||
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
|
||||
self.circuittermination1 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit1,
|
||||
term_side=CircuitTerminationSideChoices.SIDE_A,
|
||||
site=self.site1,
|
||||
port_speed=1000000
|
||||
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
)
|
||||
self.circuittermination2 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit1,
|
||||
term_side=CircuitTerminationSideChoices.SIDE_Z,
|
||||
site=self.site2,
|
||||
port_speed=1000000
|
||||
circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
|
||||
)
|
||||
self.circuittermination3 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit2,
|
||||
term_side=CircuitTerminationSideChoices.SIDE_A,
|
||||
site=self.site1,
|
||||
port_speed=1000000
|
||||
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
)
|
||||
self.circuittermination4 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit2,
|
||||
term_side=CircuitTerminationSideChoices.SIDE_Z,
|
||||
site=self.site2,
|
||||
port_speed=1000000
|
||||
circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
|
||||
)
|
||||
|
||||
def test_get_circuittermination(self):
|
||||
@@ -382,7 +366,7 @@ class CircuitTerminationTest(APITestCase):
|
||||
|
||||
data = {
|
||||
'circuit': self.circuit3.pk,
|
||||
'term_side': CircuitTerminationSideChoices.SIDE_A,
|
||||
'term_side': TERM_SIDE_A,
|
||||
'site': self.site1.pk,
|
||||
'port_speed': 1000000,
|
||||
}
|
||||
@@ -401,15 +385,12 @@ class CircuitTerminationTest(APITestCase):
|
||||
def test_update_circuittermination(self):
|
||||
|
||||
circuittermination5 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit3,
|
||||
term_side=CircuitTerminationSideChoices.SIDE_A,
|
||||
site=self.site1,
|
||||
port_speed=1000000
|
||||
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
)
|
||||
|
||||
data = {
|
||||
'circuit': self.circuit3.pk,
|
||||
'term_side': CircuitTerminationSideChoices.SIDE_Z,
|
||||
'term_side': TERM_SIDE_Z,
|
||||
'site': self.site2.pk,
|
||||
'port_speed': 1000000,
|
||||
}
|
||||
|
||||
287
netbox/circuits/tests/test_filters.py
Normal file
287
netbox/circuits/tests/test_filters.py
Normal file
@@ -0,0 +1,287 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_OFFLINE, CIRCUIT_STATUS_PLANNED
|
||||
from circuits.filters import *
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.models import Region, Site
|
||||
|
||||
|
||||
class ProviderTestCase(TestCase):
|
||||
queryset = Provider.objects.all()
|
||||
filterset = ProviderFilter
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'),
|
||||
Provider(name='Provider 2', slug='provider-2', asn=65002, account='2345'),
|
||||
Provider(name='Provider 3', slug='provider-3', asn=65003, account='3456'),
|
||||
Provider(name='Provider 4', slug='provider-4', asn=65004, account='4567'),
|
||||
Provider(name='Provider 5', slug='provider-5', asn=65005, account='5678'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
regions = (
|
||||
Region(name='Test Region 1', slug='test-region-1'),
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
circuit_types = (
|
||||
CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'),
|
||||
CircuitType(name='Test Circuit Type 2', slug='test-circuit-type-2'),
|
||||
)
|
||||
CircuitType.objects.bulk_create(circuit_types)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'),
|
||||
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 1'),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
CircuitTermination.objects.bulk_create((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A', port_speed=1000),
|
||||
))
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider 1', 'Provider 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['provider-1', 'provider-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asn(self):
|
||||
params = {'asn': ['65001', '65002']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_account(self):
|
||||
params = {'account': ['1234', '2345']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CircuitTypeTestCase(TestCase):
|
||||
queryset = CircuitType.objects.all()
|
||||
filterset = CircuitTypeFilter
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
CircuitType.objects.bulk_create((
|
||||
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
|
||||
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
|
||||
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': [self.queryset.first().pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Circuit Type 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['circuit-type-1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class CircuitTestCase(TestCase):
|
||||
queryset = Circuit.objects.all()
|
||||
filterset = CircuitFilter
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
regions = (
|
||||
Region(name='Test Region 1', slug='test-region-1'),
|
||||
Region(name='Test Region 2', slug='test-region-2'),
|
||||
Region(name='Test Region 3', slug='test-region-3'),
|
||||
)
|
||||
# Can't use bulk_create for models with MPTT fields
|
||||
for r in regions:
|
||||
r.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
|
||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
|
||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2]),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
circuit_types = (
|
||||
CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'),
|
||||
CircuitType(name='Test Circuit Type 2', slug='test-circuit-type-2'),
|
||||
)
|
||||
CircuitType.objects.bulk_create(circuit_types)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CIRCUIT_STATUS_ACTIVE),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CIRCUIT_STATUS_ACTIVE),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CIRCUIT_STATUS_PLANNED),
|
||||
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', commit_rate=4000, status=CIRCUIT_STATUS_PLANNED),
|
||||
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CIRCUIT_STATUS_OFFLINE),
|
||||
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', commit_rate=6000, status=CIRCUIT_STATUS_OFFLINE),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=1000),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=1000),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
def test_cid(self):
|
||||
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_install_date(self):
|
||||
params = {'install_date': ['2020-01-01', '2020-01-02']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_commit_rate(self):
|
||||
params = {'commit_rate': ['1000', '2000']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_provider(self):
|
||||
provider = Provider.objects.first()
|
||||
params = {'provider_id': [provider.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'provider': [provider.slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_type(self):
|
||||
circuit_type = CircuitType.objects.first()
|
||||
params = {'type_id': [circuit_type.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'type': [circuit_type.slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CircuitTerminationTestCase(TestCase):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = CircuitTerminationFilter
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1'),
|
||||
Site(name='Test Site 2', slug='test-site-2'),
|
||||
Site(name='Test Site 3', slug='test-site-3'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
circuit_types = (
|
||||
CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'),
|
||||
)
|
||||
CircuitType.objects.bulk_create(circuit_types)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 2'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 3'),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC'),
|
||||
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
def test_term_side(self):
|
||||
params = {'term_side': 'A'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_port_speed(self):
|
||||
params = {'port_speed': ['1000', '2000']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_upstream_speed(self):
|
||||
params = {'upstream_speed': ['1000', '2000']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_xconnect_id(self):
|
||||
params = {'xconnect_id': ['ABC', 'DEF']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_circuit_id(self):
|
||||
circuits = Circuit.objects.all()[:2]
|
||||
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
@@ -10,12 +10,7 @@ from utilities.testing import create_test_user
|
||||
class ProviderTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
user = create_test_user(
|
||||
permissions=[
|
||||
'circuits.view_provider',
|
||||
'circuits.add_provider',
|
||||
]
|
||||
)
|
||||
user = create_test_user(permissions=['circuits.view_provider'])
|
||||
self.client = Client()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -41,30 +36,11 @@ class ProviderTestCase(TestCase):
|
||||
response = self.client.get(provider.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_provider_import(self):
|
||||
|
||||
csv_data = (
|
||||
"name,slug",
|
||||
"Provider 4,provider-4",
|
||||
"Provider 5,provider-5",
|
||||
"Provider 6,provider-6",
|
||||
)
|
||||
|
||||
response = self.client.post(reverse('circuits:provider_import'), {'csv': '\n'.join(csv_data)})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(Provider.objects.count(), 6)
|
||||
|
||||
|
||||
class CircuitTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
user = create_test_user(
|
||||
permissions=[
|
||||
'circuits.view_circuittype',
|
||||
'circuits.add_circuittype',
|
||||
]
|
||||
)
|
||||
user = create_test_user(permissions=['circuits.view_circuittype'])
|
||||
self.client = Client()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -81,30 +57,11 @@ class CircuitTypeTestCase(TestCase):
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_circuittype_import(self):
|
||||
|
||||
csv_data = (
|
||||
"name,slug",
|
||||
"Circuit Type 4,circuit-type-4",
|
||||
"Circuit Type 5,circuit-type-5",
|
||||
"Circuit Type 6,circuit-type-6",
|
||||
)
|
||||
|
||||
response = self.client.post(reverse('circuits:circuittype_import'), {'csv': '\n'.join(csv_data)})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(CircuitType.objects.count(), 6)
|
||||
|
||||
|
||||
class CircuitTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
user = create_test_user(
|
||||
permissions=[
|
||||
'circuits.view_circuit',
|
||||
'circuits.add_circuit',
|
||||
]
|
||||
)
|
||||
user = create_test_user(permissions=['circuits.view_circuit'])
|
||||
self.client = Client()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -136,17 +93,3 @@ class CircuitTestCase(TestCase):
|
||||
circuit = Circuit.objects.first()
|
||||
response = self.client.get(circuit.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_circuit_import(self):
|
||||
|
||||
csv_data = (
|
||||
"cid,provider,type",
|
||||
"Circuit 4,Provider 1,Circuit Type 1",
|
||||
"Circuit 5,Provider 1,Circuit Type 1",
|
||||
"Circuit 6,Provider 1,Circuit Type 1",
|
||||
)
|
||||
|
||||
response = self.client.post(reverse('circuits:circuit_import'), {'csv': '\n'.join(csv_data)})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(Circuit.objects.count(), 6)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
@@ -5,14 +6,16 @@ from django.db import transaction
|
||||
from django.db.models import Count, OuterRef, Subquery
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from extras.models import Graph
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
from . import filters, forms, tables
|
||||
from .choices import CircuitTerminationSideChoices
|
||||
from .constants import TERM_SIDE_A, TERM_SIDE_Z
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
|
||||
@@ -36,11 +39,20 @@ class ProviderView(PermissionRequiredMixin, View):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
|
||||
show_graphs = Graph.objects.filter(type__model='provider').exists()
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||
|
||||
circuits_table = tables.CircuitTable(circuits, orderable=False)
|
||||
circuits_table.columns.hide('provider')
|
||||
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(circuits_table)
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
'provider': provider,
|
||||
'circuits': circuits,
|
||||
'circuits_table': circuits_table,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
|
||||
@@ -151,12 +163,12 @@ class CircuitView(PermissionRequiredMixin, View):
|
||||
termination_a = CircuitTermination.objects.prefetch_related(
|
||||
'site__region', 'connected_endpoint__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
|
||||
circuit=circuit, term_side=TERM_SIDE_A
|
||||
).first()
|
||||
termination_z = CircuitTermination.objects.prefetch_related(
|
||||
'site__region', 'connected_endpoint__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
|
||||
circuit=circuit, term_side=TERM_SIDE_Z
|
||||
).first()
|
||||
|
||||
return render(request, 'circuits/circuit.html', {
|
||||
@@ -212,12 +224,8 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
def circuit_terminations_swap(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
termination_a = CircuitTermination.objects.filter(
|
||||
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
|
||||
).first()
|
||||
termination_z = CircuitTermination.objects.filter(
|
||||
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
|
||||
).first()
|
||||
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
|
||||
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
|
||||
if not termination_a and not termination_z:
|
||||
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
@@ -4,7 +4,6 @@ from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
@@ -68,7 +67,7 @@ class RegionSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||
status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False)
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
time_zone = TimeZoneField(required=False)
|
||||
@@ -108,18 +107,18 @@ class RackRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['id', 'name', 'slug', 'color', 'description', 'rack_count']
|
||||
fields = ['id', 'name', 'slug', 'color', 'rack_count']
|
||||
|
||||
|
||||
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||
status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True)
|
||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, required=False)
|
||||
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
|
||||
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
|
||||
outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||
@@ -157,7 +156,7 @@ class RackUnitSerializer(serializers.Serializer):
|
||||
"""
|
||||
id = serializers.IntegerField(read_only=True)
|
||||
name = serializers.CharField(read_only=True)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
|
||||
face = serializers.IntegerField(read_only=True)
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
|
||||
@@ -171,31 +170,6 @@ class RackReservationSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
|
||||
|
||||
|
||||
class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
face = serializers.ChoiceField(
|
||||
choices=DeviceFaceChoices,
|
||||
default=DeviceFaceChoices.FACE_FRONT
|
||||
)
|
||||
render = serializers.ChoiceField(
|
||||
choices=RackElevationDetailRenderChoices,
|
||||
default=RackElevationDetailRenderChoices.RENDER_JSON
|
||||
)
|
||||
unit_width = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT
|
||||
)
|
||||
unit_height = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
|
||||
)
|
||||
exclude = serializers.IntegerField(
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
expand_devices = serializers.BooleanField(
|
||||
required=False,
|
||||
default=True
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
@@ -212,7 +186,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
|
||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, required=False, allow_null=True)
|
||||
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -226,72 +200,58 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
|
||||
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type']
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type']
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw']
|
||||
|
||||
|
||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
required=False
|
||||
)
|
||||
power_port = PowerPortTemplateSerializer(
|
||||
required=False
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
choices=POWERFEED_LEG_CHOICES,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg']
|
||||
fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg']
|
||||
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
|
||||
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
|
||||
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
|
||||
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'mgmt_only']
|
||||
fields = ['id', 'device_type', 'name', 'type', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
@@ -300,7 +260,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
rear_port = NestedRearPortTemplateSerializer()
|
||||
|
||||
class Meta:
|
||||
@@ -326,9 +286,7 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count',
|
||||
]
|
||||
fields = ['id', 'name', 'slug', 'color', 'vm_role', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
class PlatformSerializer(ValidatedModelSerializer):
|
||||
@@ -351,8 +309,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer()
|
||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
@@ -412,51 +370,43 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
return obj.get_config_context()
|
||||
|
||||
|
||||
class DeviceNAPALMSerializer(serializers.Serializer):
|
||||
method = serializers.DictField()
|
||||
|
||||
|
||||
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
|
||||
'connection_status', 'cable', 'tags',
|
||||
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||
'cable', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
|
||||
'connection_status', 'cable', 'tags',
|
||||
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||
'cable', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortSerializer(
|
||||
required=False
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
choices=POWERFEED_LEG_CHOICES,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
@@ -470,33 +420,31 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
|
||||
'id', 'device', 'name', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
|
||||
'id', 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
|
||||
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
|
||||
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
|
||||
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@@ -510,9 +458,9 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
|
||||
'tagged_vlans', 'tags', 'count_ipaddresses',
|
||||
'id', 'device', 'name', 'type', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
|
||||
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode',
|
||||
'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
|
||||
]
|
||||
|
||||
# TODO: This validation should be handled by Interface.clean()
|
||||
@@ -538,7 +486,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
|
||||
class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
@@ -560,7 +508,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
rear_port = FrontPortRearPortSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
@@ -612,8 +560,8 @@ class CableSerializer(ValidatedModelSerializer):
|
||||
)
|
||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||
status = ChoiceField(choices=CableStatusChoices, required=False)
|
||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
|
||||
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
@@ -718,20 +666,20 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
default=None
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=PowerFeedTypeChoices,
|
||||
default=PowerFeedTypeChoices.TYPE_PRIMARY
|
||||
choices=POWERFEED_TYPE_CHOICES,
|
||||
default=POWERFEED_TYPE_PRIMARY
|
||||
)
|
||||
status = ChoiceField(
|
||||
choices=PowerFeedStatusChoices,
|
||||
default=PowerFeedStatusChoices.STATUS_ACTIVE
|
||||
choices=POWERFEED_STATUS_CHOICES,
|
||||
default=POWERFEED_STATUS_ACTIVE
|
||||
)
|
||||
supply = ChoiceField(
|
||||
choices=PowerFeedSupplyChoices,
|
||||
default=PowerFeedSupplyChoices.SUPPLY_AC
|
||||
choices=POWERFEED_SUPPLY_CHOICES,
|
||||
default=POWERFEED_SUPPLY_AC
|
||||
)
|
||||
phase = ChoiceField(
|
||||
choices=PowerFeedPhaseChoices,
|
||||
default=PowerFeedPhaseChoices.PHASE_SINGLE
|
||||
choices=POWERFEED_PHASE_CHOICES,
|
||||
default=POWERFEED_PHASE_SINGLE
|
||||
)
|
||||
tags = TagListSerializerField(
|
||||
required=False
|
||||
|
||||
@@ -2,8 +2,8 @@ from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, F
|
||||
from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, reverse
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.openapi import Parameter
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
@@ -13,7 +13,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim import constants, filters
|
||||
from dcim import filters
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
@@ -23,12 +23,12 @@ from dcim.models import (
|
||||
)
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.models import Graph
|
||||
from ipam.models import Prefix, VLAN
|
||||
from utilities.api import (
|
||||
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
|
||||
)
|
||||
from utilities.custom_inspectors import NullablePaginatorInspector
|
||||
from utilities.utils import get_subquery
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import serializers
|
||||
@@ -42,20 +42,16 @@ from .exceptions import MissingFilterException
|
||||
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
|
||||
(ConsolePort, ['type', 'connection_status']),
|
||||
(ConsolePortTemplate, ['type']),
|
||||
(ConsoleServerPort, ['type']),
|
||||
(ConsoleServerPortTemplate, ['type']),
|
||||
(ConsolePort, ['connection_status']),
|
||||
(Device, ['face', 'status']),
|
||||
(DeviceType, ['subdevice_role']),
|
||||
(FrontPort, ['type']),
|
||||
(FrontPortTemplate, ['type']),
|
||||
(Interface, ['type', 'mode']),
|
||||
(InterfaceTemplate, ['type']),
|
||||
(PowerOutlet, ['type', 'feed_leg']),
|
||||
(PowerOutletTemplate, ['type', 'feed_leg']),
|
||||
(PowerPort, ['type', 'connection_status']),
|
||||
(PowerPortTemplate, ['type']),
|
||||
(PowerOutlet, ['feed_leg']),
|
||||
(PowerOutletTemplate, ['feed_leg']),
|
||||
(PowerPort, ['connection_status']),
|
||||
(Rack, ['outer_unit', 'status', 'type', 'width']),
|
||||
(RearPort, ['type']),
|
||||
(RearPortTemplate, ['type']),
|
||||
@@ -133,7 +129,7 @@ class SiteViewSet(CustomFieldModelViewSet):
|
||||
A convenience method for rendering graphs for a particular site.
|
||||
"""
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='site')
|
||||
queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE)
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -176,15 +172,13 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
serializer_class = serializers.RackSerializer
|
||||
filterset_class = filters.RackFilter
|
||||
|
||||
@swagger_auto_schema(deprecated=True)
|
||||
@action(detail=True)
|
||||
def units(self, request, pk=None):
|
||||
"""
|
||||
List rack units (by rack)
|
||||
"""
|
||||
# TODO: Remove this action detail route in v2.8
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
face = request.GET.get('face', 'front')
|
||||
face = request.GET.get('face', 0)
|
||||
exclude_pk = request.GET.get('exclude', None)
|
||||
if exclude_pk is not None:
|
||||
try:
|
||||
@@ -203,39 +197,6 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
|
||||
return self.get_paginated_response(rack_units.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
responses={200: serializers.RackUnitSerializer(many=True)},
|
||||
query_serializer=serializers.RackElevationDetailFilterSerializer
|
||||
)
|
||||
@action(detail=True)
|
||||
def elevation(self, request, pk=None):
|
||||
"""
|
||||
Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
|
||||
"""
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, 400)
|
||||
data = serializer.validated_data
|
||||
|
||||
if data['render'] == 'svg':
|
||||
# Render and return the elevation as an SVG drawing with the correct content type
|
||||
drawing = rack.get_elevation_svg(data['face'], data['unit_width'], data['unit_height'])
|
||||
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
|
||||
|
||||
else:
|
||||
# Return a JSON representation of the rack units in the elevation
|
||||
elevation = rack.get_rack_units(
|
||||
face=data['face'],
|
||||
exclude=data['exclude'],
|
||||
expand_devices=data['expand_devices']
|
||||
)
|
||||
|
||||
page = self.paginate_queryset(elevation)
|
||||
if page is not None:
|
||||
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
|
||||
return self.get_paginated_response(rack_units.data)
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
@@ -392,11 +353,22 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
A convenience method for rendering graphs for a particular Device.
|
||||
"""
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='device')
|
||||
queryset = Graph.objects.filter(type=GRAPH_TYPE_DEVICE)
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
Parameter(
|
||||
name='method',
|
||||
in_='query',
|
||||
required=True,
|
||||
type=openapi.TYPE_STRING
|
||||
)
|
||||
],
|
||||
responses={'200': serializers.DeviceNAPALMSerializer}
|
||||
)
|
||||
@action(detail=True, url_path='napalm')
|
||||
def napalm(self, request, pk):
|
||||
"""
|
||||
@@ -435,13 +407,29 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
napalm_methods = request.GET.getlist('method')
|
||||
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||
ip_address = str(device.primary_ip.address.ip)
|
||||
username = settings.NAPALM_USERNAME
|
||||
password = settings.NAPALM_PASSWORD
|
||||
optional_args = settings.NAPALM_ARGS.copy()
|
||||
if device.platform.napalm_args is not None:
|
||||
optional_args.update(device.platform.napalm_args)
|
||||
|
||||
# Update NAPALM parameters according to the request headers
|
||||
for header in request.headers:
|
||||
if header[:9].lower() != 'x-napalm-':
|
||||
continue
|
||||
|
||||
key = header[9:]
|
||||
if key.lower() == 'username':
|
||||
username = request.headers[header]
|
||||
elif key.lower() == 'password':
|
||||
password = request.headers[header]
|
||||
elif key:
|
||||
optional_args[key.lower()] = request.headers[header]
|
||||
|
||||
d = driver(
|
||||
hostname=ip_address,
|
||||
username=settings.NAPALM_USERNAME,
|
||||
password=settings.NAPALM_PASSWORD,
|
||||
username=username,
|
||||
password=password,
|
||||
timeout=settings.NAPALM_TIMEOUT,
|
||||
optional_args=optional_args
|
||||
)
|
||||
@@ -514,7 +502,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
|
||||
A convenience method for rendering graphs for a particular interface.
|
||||
"""
|
||||
interface = get_object_or_404(Interface, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='interface')
|
||||
queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE)
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,383 @@
|
||||
from .choices import InterfaceTypeChoices
|
||||
|
||||
# BGP ASN bounds
|
||||
BGP_ASN_MIN = 1
|
||||
BGP_ASN_MAX = 2**32 - 1
|
||||
|
||||
#
|
||||
# Interface type groups
|
||||
#
|
||||
# Rack types
|
||||
RACK_TYPE_2POST = 100
|
||||
RACK_TYPE_4POST = 200
|
||||
RACK_TYPE_CABINET = 300
|
||||
RACK_TYPE_WALLFRAME = 1000
|
||||
RACK_TYPE_WALLCABINET = 1100
|
||||
RACK_TYPE_CHOICES = (
|
||||
(RACK_TYPE_2POST, '2-post frame'),
|
||||
(RACK_TYPE_4POST, '4-post frame'),
|
||||
(RACK_TYPE_CABINET, '4-post cabinet'),
|
||||
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
|
||||
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
|
||||
)
|
||||
|
||||
# Rack widths
|
||||
RACK_WIDTH_19IN = 19
|
||||
RACK_WIDTH_23IN = 23
|
||||
RACK_WIDTH_CHOICES = (
|
||||
(RACK_WIDTH_19IN, '19 inches'),
|
||||
(RACK_WIDTH_23IN, '23 inches'),
|
||||
)
|
||||
|
||||
# Rack faces
|
||||
RACK_FACE_FRONT = 0
|
||||
RACK_FACE_REAR = 1
|
||||
RACK_FACE_CHOICES = [
|
||||
[RACK_FACE_FRONT, 'Front'],
|
||||
[RACK_FACE_REAR, 'Rear'],
|
||||
]
|
||||
|
||||
# Rack statuses
|
||||
RACK_STATUS_RESERVED = 0
|
||||
RACK_STATUS_AVAILABLE = 1
|
||||
RACK_STATUS_PLANNED = 2
|
||||
RACK_STATUS_ACTIVE = 3
|
||||
RACK_STATUS_DEPRECATED = 4
|
||||
RACK_STATUS_CHOICES = [
|
||||
[RACK_STATUS_ACTIVE, 'Active'],
|
||||
[RACK_STATUS_PLANNED, 'Planned'],
|
||||
[RACK_STATUS_RESERVED, 'Reserved'],
|
||||
[RACK_STATUS_AVAILABLE, 'Available'],
|
||||
[RACK_STATUS_DEPRECATED, 'Deprecated'],
|
||||
]
|
||||
|
||||
# Device rack position
|
||||
DEVICE_POSITION_CHOICES = [
|
||||
# Rack.u_height is limited to 100
|
||||
(i, 'Unit {}'.format(i)) for i in range(1, 101)
|
||||
]
|
||||
|
||||
# Parent/child device roles
|
||||
SUBDEVICE_ROLE_PARENT = True
|
||||
SUBDEVICE_ROLE_CHILD = False
|
||||
SUBDEVICE_ROLE_CHOICES = (
|
||||
(None, 'None'),
|
||||
(SUBDEVICE_ROLE_PARENT, 'Parent'),
|
||||
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||
)
|
||||
|
||||
# Interface types
|
||||
# Virtual
|
||||
IFACE_TYPE_VIRTUAL = 0
|
||||
IFACE_TYPE_LAG = 200
|
||||
# Ethernet
|
||||
IFACE_TYPE_100ME_FIXED = 800
|
||||
IFACE_TYPE_1GE_FIXED = 1000
|
||||
IFACE_TYPE_1GE_GBIC = 1050
|
||||
IFACE_TYPE_1GE_SFP = 1100
|
||||
IFACE_TYPE_2GE_FIXED = 1120
|
||||
IFACE_TYPE_5GE_FIXED = 1130
|
||||
IFACE_TYPE_10GE_FIXED = 1150
|
||||
IFACE_TYPE_10GE_CX4 = 1170
|
||||
IFACE_TYPE_10GE_SFP_PLUS = 1200
|
||||
IFACE_TYPE_10GE_XFP = 1300
|
||||
IFACE_TYPE_10GE_XENPAK = 1310
|
||||
IFACE_TYPE_10GE_X2 = 1320
|
||||
IFACE_TYPE_25GE_SFP28 = 1350
|
||||
IFACE_TYPE_40GE_QSFP_PLUS = 1400
|
||||
IFACE_TYPE_50GE_QSFP28 = 1420
|
||||
IFACE_TYPE_100GE_CFP = 1500
|
||||
IFACE_TYPE_100GE_CFP2 = 1510
|
||||
IFACE_TYPE_100GE_CFP4 = 1520
|
||||
IFACE_TYPE_100GE_CPAK = 1550
|
||||
IFACE_TYPE_100GE_QSFP28 = 1600
|
||||
IFACE_TYPE_200GE_CFP2 = 1650
|
||||
IFACE_TYPE_200GE_QSFP56 = 1700
|
||||
IFACE_TYPE_400GE_QSFP_DD = 1750
|
||||
IFACE_TYPE_400GE_OSFP = 1800
|
||||
# Wireless
|
||||
IFACE_TYPE_80211A = 2600
|
||||
IFACE_TYPE_80211G = 2610
|
||||
IFACE_TYPE_80211N = 2620
|
||||
IFACE_TYPE_80211AC = 2630
|
||||
IFACE_TYPE_80211AD = 2640
|
||||
# Cellular
|
||||
IFACE_TYPE_GSM = 2810
|
||||
IFACE_TYPE_CDMA = 2820
|
||||
IFACE_TYPE_LTE = 2830
|
||||
# SONET
|
||||
IFACE_TYPE_SONET_OC3 = 6100
|
||||
IFACE_TYPE_SONET_OC12 = 6200
|
||||
IFACE_TYPE_SONET_OC48 = 6300
|
||||
IFACE_TYPE_SONET_OC192 = 6400
|
||||
IFACE_TYPE_SONET_OC768 = 6500
|
||||
IFACE_TYPE_SONET_OC1920 = 6600
|
||||
IFACE_TYPE_SONET_OC3840 = 6700
|
||||
# Fibrechannel
|
||||
IFACE_TYPE_1GFC_SFP = 3010
|
||||
IFACE_TYPE_2GFC_SFP = 3020
|
||||
IFACE_TYPE_4GFC_SFP = 3040
|
||||
IFACE_TYPE_8GFC_SFP_PLUS = 3080
|
||||
IFACE_TYPE_16GFC_SFP_PLUS = 3160
|
||||
IFACE_TYPE_32GFC_SFP28 = 3320
|
||||
IFACE_TYPE_128GFC_QSFP28 = 3400
|
||||
# InfiniBand
|
||||
IFACE_FF_INFINIBAND_SDR = 7010
|
||||
IFACE_FF_INFINIBAND_DDR = 7020
|
||||
IFACE_FF_INFINIBAND_QDR = 7030
|
||||
IFACE_FF_INFINIBAND_FDR10 = 7040
|
||||
IFACE_FF_INFINIBAND_FDR = 7050
|
||||
IFACE_FF_INFINIBAND_EDR = 7060
|
||||
IFACE_FF_INFINIBAND_HDR = 7070
|
||||
IFACE_FF_INFINIBAND_NDR = 7080
|
||||
IFACE_FF_INFINIBAND_XDR = 7090
|
||||
# Serial
|
||||
IFACE_TYPE_T1 = 4000
|
||||
IFACE_TYPE_E1 = 4010
|
||||
IFACE_TYPE_T3 = 4040
|
||||
IFACE_TYPE_E3 = 4050
|
||||
# Stacking
|
||||
IFACE_TYPE_STACKWISE = 5000
|
||||
IFACE_TYPE_STACKWISE_PLUS = 5050
|
||||
IFACE_TYPE_FLEXSTACK = 5100
|
||||
IFACE_TYPE_FLEXSTACK_PLUS = 5150
|
||||
IFACE_TYPE_JUNIPER_VCP = 5200
|
||||
IFACE_TYPE_SUMMITSTACK = 5300
|
||||
IFACE_TYPE_SUMMITSTACK128 = 5310
|
||||
IFACE_TYPE_SUMMITSTACK256 = 5320
|
||||
IFACE_TYPE_SUMMITSTACK512 = 5330
|
||||
|
||||
# Other
|
||||
IFACE_TYPE_OTHER = 32767
|
||||
|
||||
IFACE_TYPE_CHOICES = [
|
||||
[
|
||||
'Virtual interfaces',
|
||||
[
|
||||
[IFACE_TYPE_VIRTUAL, 'Virtual'],
|
||||
[IFACE_TYPE_LAG, 'Link Aggregation Group (LAG)'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'Ethernet (fixed)',
|
||||
[
|
||||
[IFACE_TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'],
|
||||
[IFACE_TYPE_1GE_FIXED, '1000BASE-T (1GE)'],
|
||||
[IFACE_TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'],
|
||||
[IFACE_TYPE_5GE_FIXED, '5GBASE-T (5GE)'],
|
||||
[IFACE_TYPE_10GE_FIXED, '10GBASE-T (10GE)'],
|
||||
[IFACE_TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Ethernet (modular)',
|
||||
[
|
||||
[IFACE_TYPE_1GE_GBIC, 'GBIC (1GE)'],
|
||||
[IFACE_TYPE_1GE_SFP, 'SFP (1GE)'],
|
||||
[IFACE_TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'],
|
||||
[IFACE_TYPE_10GE_XFP, 'XFP (10GE)'],
|
||||
[IFACE_TYPE_10GE_XENPAK, 'XENPAK (10GE)'],
|
||||
[IFACE_TYPE_10GE_X2, 'X2 (10GE)'],
|
||||
[IFACE_TYPE_25GE_SFP28, 'SFP28 (25GE)'],
|
||||
[IFACE_TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||
[IFACE_TYPE_50GE_QSFP28, 'QSFP28 (50GE)'],
|
||||
[IFACE_TYPE_100GE_CFP, 'CFP (100GE)'],
|
||||
[IFACE_TYPE_100GE_CFP2, 'CFP2 (100GE)'],
|
||||
[IFACE_TYPE_200GE_CFP2, 'CFP2 (200GE)'],
|
||||
[IFACE_TYPE_100GE_CFP4, 'CFP4 (100GE)'],
|
||||
[IFACE_TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'],
|
||||
[IFACE_TYPE_100GE_QSFP28, 'QSFP28 (100GE)'],
|
||||
[IFACE_TYPE_200GE_QSFP56, 'QSFP56 (200GE)'],
|
||||
[IFACE_TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
|
||||
[IFACE_TYPE_400GE_OSFP, 'OSFP (400GE)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Wireless',
|
||||
[
|
||||
[IFACE_TYPE_80211A, 'IEEE 802.11a'],
|
||||
[IFACE_TYPE_80211G, 'IEEE 802.11b/g'],
|
||||
[IFACE_TYPE_80211N, 'IEEE 802.11n'],
|
||||
[IFACE_TYPE_80211AC, 'IEEE 802.11ac'],
|
||||
[IFACE_TYPE_80211AD, 'IEEE 802.11ad'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Cellular',
|
||||
[
|
||||
[IFACE_TYPE_GSM, 'GSM'],
|
||||
[IFACE_TYPE_CDMA, 'CDMA'],
|
||||
[IFACE_TYPE_LTE, 'LTE'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'SONET',
|
||||
[
|
||||
[IFACE_TYPE_SONET_OC3, 'OC-3/STM-1'],
|
||||
[IFACE_TYPE_SONET_OC12, 'OC-12/STM-4'],
|
||||
[IFACE_TYPE_SONET_OC48, 'OC-48/STM-16'],
|
||||
[IFACE_TYPE_SONET_OC192, 'OC-192/STM-64'],
|
||||
[IFACE_TYPE_SONET_OC768, 'OC-768/STM-256'],
|
||||
[IFACE_TYPE_SONET_OC1920, 'OC-1920/STM-640'],
|
||||
[IFACE_TYPE_SONET_OC3840, 'OC-3840/STM-1234'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'FibreChannel',
|
||||
[
|
||||
[IFACE_TYPE_1GFC_SFP, 'SFP (1GFC)'],
|
||||
[IFACE_TYPE_2GFC_SFP, 'SFP (2GFC)'],
|
||||
[IFACE_TYPE_4GFC_SFP, 'SFP (4GFC)'],
|
||||
[IFACE_TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
|
||||
[IFACE_TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
|
||||
[IFACE_TYPE_32GFC_SFP28, 'SFP28 (32GFC)'],
|
||||
[IFACE_TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'InfiniBand',
|
||||
[
|
||||
[IFACE_FF_INFINIBAND_SDR, 'SDR (2 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_DDR, 'DDR (4 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_QDR, 'QDR (8 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_EDR, 'EDR (25 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_HDR, 'HDR (50 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_NDR, 'NDR (100 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_XDR, 'XDR (250 Gbps)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Serial',
|
||||
[
|
||||
[IFACE_TYPE_T1, 'T1 (1.544 Mbps)'],
|
||||
[IFACE_TYPE_E1, 'E1 (2.048 Mbps)'],
|
||||
[IFACE_TYPE_T3, 'T3 (45 Mbps)'],
|
||||
[IFACE_TYPE_E3, 'E3 (34 Mbps)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Stacking',
|
||||
[
|
||||
[IFACE_TYPE_STACKWISE, 'Cisco StackWise'],
|
||||
[IFACE_TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||
[IFACE_TYPE_FLEXSTACK, 'Cisco FlexStack'],
|
||||
[IFACE_TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||
[IFACE_TYPE_JUNIPER_VCP, 'Juniper VCP'],
|
||||
[IFACE_TYPE_SUMMITSTACK, 'Extreme SummitStack'],
|
||||
[IFACE_TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'],
|
||||
[IFACE_TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'],
|
||||
[IFACE_TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Other',
|
||||
[
|
||||
[IFACE_TYPE_OTHER, 'Other'],
|
||||
]
|
||||
],
|
||||
]
|
||||
|
||||
VIRTUAL_IFACE_TYPES = [
|
||||
InterfaceTypeChoices.TYPE_VIRTUAL,
|
||||
InterfaceTypeChoices.TYPE_LAG,
|
||||
IFACE_TYPE_VIRTUAL,
|
||||
IFACE_TYPE_LAG,
|
||||
]
|
||||
|
||||
WIRELESS_IFACE_TYPES = [
|
||||
InterfaceTypeChoices.TYPE_80211A,
|
||||
InterfaceTypeChoices.TYPE_80211G,
|
||||
InterfaceTypeChoices.TYPE_80211N,
|
||||
InterfaceTypeChoices.TYPE_80211AC,
|
||||
InterfaceTypeChoices.TYPE_80211AD,
|
||||
IFACE_TYPE_80211A,
|
||||
IFACE_TYPE_80211G,
|
||||
IFACE_TYPE_80211N,
|
||||
IFACE_TYPE_80211AC,
|
||||
IFACE_TYPE_80211AD,
|
||||
]
|
||||
|
||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||
|
||||
IFACE_MODE_ACCESS = 100
|
||||
IFACE_MODE_TAGGED = 200
|
||||
IFACE_MODE_TAGGED_ALL = 300
|
||||
IFACE_MODE_CHOICES = [
|
||||
[IFACE_MODE_ACCESS, 'Access'],
|
||||
[IFACE_MODE_TAGGED, 'Tagged'],
|
||||
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
|
||||
]
|
||||
|
||||
# Pass-through port types
|
||||
PORT_TYPE_8P8C = 1000
|
||||
PORT_TYPE_110_PUNCH = 1100
|
||||
PORT_TYPE_BNC = 1200
|
||||
PORT_TYPE_ST = 2000
|
||||
PORT_TYPE_SC = 2100
|
||||
PORT_TYPE_SC_APC = 2110
|
||||
PORT_TYPE_FC = 2200
|
||||
PORT_TYPE_LC = 2300
|
||||
PORT_TYPE_LC_APC = 2310
|
||||
PORT_TYPE_MTRJ = 2400
|
||||
PORT_TYPE_MPO = 2500
|
||||
PORT_TYPE_LSH = 2600
|
||||
PORT_TYPE_LSH_APC = 2610
|
||||
PORT_TYPE_CHOICES = [
|
||||
[
|
||||
'Copper',
|
||||
[
|
||||
[PORT_TYPE_8P8C, '8P8C'],
|
||||
[PORT_TYPE_110_PUNCH, '110 Punch'],
|
||||
[PORT_TYPE_BNC, 'BNC'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'Fiber Optic',
|
||||
[
|
||||
[PORT_TYPE_FC, 'FC'],
|
||||
[PORT_TYPE_LC, 'LC'],
|
||||
[PORT_TYPE_LC_APC, 'LC/APC'],
|
||||
[PORT_TYPE_LSH, 'LSH'],
|
||||
[PORT_TYPE_LSH_APC, 'LSH/APC'],
|
||||
[PORT_TYPE_MPO, 'MPO'],
|
||||
[PORT_TYPE_MTRJ, 'MTRJ'],
|
||||
[PORT_TYPE_SC, 'SC'],
|
||||
[PORT_TYPE_SC_APC, 'SC/APC'],
|
||||
[PORT_TYPE_ST, 'ST'],
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
# Device statuses
|
||||
DEVICE_STATUS_OFFLINE = 0
|
||||
DEVICE_STATUS_ACTIVE = 1
|
||||
DEVICE_STATUS_PLANNED = 2
|
||||
DEVICE_STATUS_STAGED = 3
|
||||
DEVICE_STATUS_FAILED = 4
|
||||
DEVICE_STATUS_INVENTORY = 5
|
||||
DEVICE_STATUS_DECOMMISSIONING = 6
|
||||
DEVICE_STATUS_CHOICES = [
|
||||
[DEVICE_STATUS_ACTIVE, 'Active'],
|
||||
[DEVICE_STATUS_OFFLINE, 'Offline'],
|
||||
[DEVICE_STATUS_PLANNED, 'Planned'],
|
||||
[DEVICE_STATUS_STAGED, 'Staged'],
|
||||
[DEVICE_STATUS_FAILED, 'Failed'],
|
||||
[DEVICE_STATUS_INVENTORY, 'Inventory'],
|
||||
[DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'],
|
||||
]
|
||||
|
||||
# Site statuses
|
||||
SITE_STATUS_ACTIVE = 1
|
||||
SITE_STATUS_PLANNED = 2
|
||||
SITE_STATUS_RETIRED = 4
|
||||
SITE_STATUS_CHOICES = [
|
||||
[SITE_STATUS_ACTIVE, 'Active'],
|
||||
[SITE_STATUS_PLANNED, 'Planned'],
|
||||
[SITE_STATUS_RETIRED, 'Retired'],
|
||||
]
|
||||
|
||||
# Bootstrap CSS classes for device/rack statuses
|
||||
STATUS_CLASSES = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
3: 'primary',
|
||||
4: 'danger',
|
||||
5: 'default',
|
||||
6: 'warning',
|
||||
}
|
||||
|
||||
# Console/power/interface connection statuses
|
||||
CONNECTION_STATUS_PLANNED = False
|
||||
CONNECTION_STATUS_CONNECTED = True
|
||||
@@ -34,6 +392,56 @@ CABLE_TERMINATION_TYPES = [
|
||||
'circuittermination', 'powerfeed',
|
||||
]
|
||||
|
||||
# Cable types
|
||||
CABLE_TYPE_CAT3 = 1300
|
||||
CABLE_TYPE_CAT5 = 1500
|
||||
CABLE_TYPE_CAT5E = 1510
|
||||
CABLE_TYPE_CAT6 = 1600
|
||||
CABLE_TYPE_CAT6A = 1610
|
||||
CABLE_TYPE_CAT7 = 1700
|
||||
CABLE_TYPE_DAC_ACTIVE = 1800
|
||||
CABLE_TYPE_DAC_PASSIVE = 1810
|
||||
CABLE_TYPE_COAXIAL = 1900
|
||||
CABLE_TYPE_MMF = 3000
|
||||
CABLE_TYPE_MMF_OM1 = 3010
|
||||
CABLE_TYPE_MMF_OM2 = 3020
|
||||
CABLE_TYPE_MMF_OM3 = 3030
|
||||
CABLE_TYPE_MMF_OM4 = 3040
|
||||
CABLE_TYPE_SMF = 3500
|
||||
CABLE_TYPE_SMF_OS1 = 3510
|
||||
CABLE_TYPE_SMF_OS2 = 3520
|
||||
CABLE_TYPE_AOC = 3800
|
||||
CABLE_TYPE_POWER = 5000
|
||||
CABLE_TYPE_CHOICES = (
|
||||
(
|
||||
'Copper', (
|
||||
(CABLE_TYPE_CAT3, 'CAT3'),
|
||||
(CABLE_TYPE_CAT5, 'CAT5'),
|
||||
(CABLE_TYPE_CAT5E, 'CAT5e'),
|
||||
(CABLE_TYPE_CAT6, 'CAT6'),
|
||||
(CABLE_TYPE_CAT6A, 'CAT6a'),
|
||||
(CABLE_TYPE_CAT7, 'CAT7'),
|
||||
(CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
|
||||
(CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
|
||||
(CABLE_TYPE_COAXIAL, 'Coaxial'),
|
||||
),
|
||||
),
|
||||
(
|
||||
'Fiber', (
|
||||
(CABLE_TYPE_MMF, 'Multimode Fiber'),
|
||||
(CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
|
||||
(CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
|
||||
(CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
|
||||
(CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
|
||||
(CABLE_TYPE_SMF, 'Singlemode Fiber'),
|
||||
(CABLE_TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
|
||||
(CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
|
||||
(CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
|
||||
),
|
||||
),
|
||||
(CABLE_TYPE_POWER, 'Power'),
|
||||
)
|
||||
|
||||
CABLE_TERMINATION_TYPE_CHOICES = {
|
||||
# (API endpoint, human-friendly name)
|
||||
'consoleport': ('console-ports', 'Console port'),
|
||||
@@ -56,68 +464,56 @@ COMPATIBLE_TERMINATION_TYPES = {
|
||||
'circuittermination': ['interface', 'frontport', 'rearport'],
|
||||
}
|
||||
|
||||
LENGTH_UNIT_METER = 1200
|
||||
LENGTH_UNIT_CENTIMETER = 1100
|
||||
LENGTH_UNIT_MILLIMETER = 1000
|
||||
LENGTH_UNIT_FOOT = 2100
|
||||
LENGTH_UNIT_INCH = 2000
|
||||
CABLE_LENGTH_UNIT_CHOICES = (
|
||||
(LENGTH_UNIT_METER, 'Meters'),
|
||||
(LENGTH_UNIT_CENTIMETER, 'Centimeters'),
|
||||
(LENGTH_UNIT_FOOT, 'Feet'),
|
||||
(LENGTH_UNIT_INCH, 'Inches'),
|
||||
)
|
||||
RACK_DIMENSION_UNIT_CHOICES = (
|
||||
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
|
||||
(LENGTH_UNIT_INCH, 'Inches'),
|
||||
)
|
||||
|
||||
RACK_ELEVATION_STYLE = """
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
rect {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
text {
|
||||
text-anchor: middle;
|
||||
dominant-baseline: middle;
|
||||
}
|
||||
.rack {
|
||||
background-color: #f0f0f0;
|
||||
fill: none;
|
||||
stroke: black;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
.slot {
|
||||
fill: #f7f7f7;
|
||||
stroke: #a0a0a0;
|
||||
}
|
||||
.slot:hover {
|
||||
fill: #fff;
|
||||
}
|
||||
.slot+.add-device {
|
||||
fill: none;
|
||||
}
|
||||
.slot:hover+.add-device {
|
||||
fill: blue;
|
||||
}
|
||||
.add-device:hover {
|
||||
fill: blue;
|
||||
}
|
||||
.add-device:hover+.slot {
|
||||
fill: #fff;
|
||||
}
|
||||
.reserved {
|
||||
fill: url(#reserved);
|
||||
}
|
||||
.reserved:hover {
|
||||
fill: url(#reserved);
|
||||
}
|
||||
.occupied {
|
||||
fill: url(#occupied);
|
||||
}
|
||||
.occupied:hover {
|
||||
fill: url(#occupied);
|
||||
}
|
||||
.blocked {
|
||||
fill: url(#blocked);
|
||||
}
|
||||
.blocked:hover {
|
||||
fill: url(#blocked);
|
||||
}
|
||||
.blocked:hover+.add-device {
|
||||
fill: none;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# Rack Elevation SVG Size
|
||||
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
|
||||
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20
|
||||
# Power feeds
|
||||
POWERFEED_TYPE_PRIMARY = 1
|
||||
POWERFEED_TYPE_REDUNDANT = 2
|
||||
POWERFEED_TYPE_CHOICES = (
|
||||
(POWERFEED_TYPE_PRIMARY, 'Primary'),
|
||||
(POWERFEED_TYPE_REDUNDANT, 'Redundant'),
|
||||
)
|
||||
POWERFEED_SUPPLY_AC = 1
|
||||
POWERFEED_SUPPLY_DC = 2
|
||||
POWERFEED_SUPPLY_CHOICES = (
|
||||
(POWERFEED_SUPPLY_AC, 'AC'),
|
||||
(POWERFEED_SUPPLY_DC, 'DC'),
|
||||
)
|
||||
POWERFEED_PHASE_SINGLE = 1
|
||||
POWERFEED_PHASE_3PHASE = 3
|
||||
POWERFEED_PHASE_CHOICES = (
|
||||
(POWERFEED_PHASE_SINGLE, 'Single phase'),
|
||||
(POWERFEED_PHASE_3PHASE, 'Three-phase'),
|
||||
)
|
||||
POWERFEED_STATUS_OFFLINE = 0
|
||||
POWERFEED_STATUS_ACTIVE = 1
|
||||
POWERFEED_STATUS_PLANNED = 2
|
||||
POWERFEED_STATUS_FAILED = 4
|
||||
POWERFEED_STATUS_CHOICES = (
|
||||
(POWERFEED_STATUS_ACTIVE, 'Active'),
|
||||
(POWERFEED_STATUS_OFFLINE, 'Offline'),
|
||||
(POWERFEED_STATUS_PLANNED, 'Planned'),
|
||||
(POWERFEED_STATUS_FAILED, 'Failed'),
|
||||
)
|
||||
POWERFEED_LEG_A = 1
|
||||
POWERFEED_LEG_B = 2
|
||||
POWERFEED_LEG_C = 3
|
||||
POWERFEED_LEG_CHOICES = (
|
||||
(POWERFEED_LEG_A, 'A'),
|
||||
(POWERFEED_LEG_B, 'B'),
|
||||
(POWERFEED_LEG_C, 'C'),
|
||||
)
|
||||
|
||||
@@ -3,14 +3,21 @@ from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
from netaddr import AddrFormatError, EUI, mac_unix_expanded
|
||||
|
||||
from .constants import *
|
||||
|
||||
|
||||
class ASNField(models.BigIntegerField):
|
||||
description = "32-bit ASN field"
|
||||
default_validators = [
|
||||
MinValueValidator(1),
|
||||
MaxValueValidator(4294967295),
|
||||
MinValueValidator(BGP_ASN_MIN),
|
||||
MaxValueValidator(BGP_ASN_MAX),
|
||||
]
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'min_value': BGP_ASN_MIN, 'max_value': BGP_ASN_MAX}
|
||||
defaults.update(**kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
||||
word_fmt = '%.2X'
|
||||
@@ -22,7 +29,7 @@ class MACAddressField(models.Field):
|
||||
def python_type(self):
|
||||
return EUI
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
return self.to_python(value)
|
||||
|
||||
def to_python(self, value):
|
||||
|
||||
@@ -11,7 +11,6 @@ from utilities.filters import (
|
||||
TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
@@ -22,6 +21,45 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
'CableFilter',
|
||||
'ConsoleConnectionFilter',
|
||||
'ConsolePortFilter',
|
||||
'ConsolePortTemplateFilter',
|
||||
'ConsoleServerPortFilter',
|
||||
'ConsoleServerPortTemplateFilter',
|
||||
'DeviceBayFilter',
|
||||
'DeviceBayTemplateFilter',
|
||||
'DeviceFilter',
|
||||
'DeviceRoleFilter',
|
||||
'DeviceTypeFilter',
|
||||
'FrontPortFilter',
|
||||
'FrontPortTemplateFilter',
|
||||
'InterfaceConnectionFilter',
|
||||
'InterfaceFilter',
|
||||
'InterfaceTemplateFilter',
|
||||
'InventoryItemFilter',
|
||||
'ManufacturerFilter',
|
||||
'PlatformFilter',
|
||||
'PowerConnectionFilter',
|
||||
'PowerFeedFilter',
|
||||
'PowerOutletFilter',
|
||||
'PowerOutletTemplateFilter',
|
||||
'PowerPanelFilter',
|
||||
'PowerPortFilter',
|
||||
'PowerPortTemplateFilter',
|
||||
'RackFilter',
|
||||
'RackGroupFilter',
|
||||
'RackReservationFilter',
|
||||
'RackRoleFilter',
|
||||
'RearPortFilter',
|
||||
'RearPortTemplateFilter',
|
||||
'RegionFilter',
|
||||
'SiteFilter',
|
||||
'VirtualChassisFilter',
|
||||
)
|
||||
|
||||
|
||||
class RegionFilter(NameSlugSearchFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -49,7 +87,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
label='Search',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=SiteStatusChoices,
|
||||
choices=SITE_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
@@ -94,6 +132,17 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
|
||||
|
||||
class RackGroupFilter(NameSlugSearchFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -126,6 +175,17 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -147,7 +207,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
label='Group',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=RackStatusChoices,
|
||||
choices=RACK_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -347,28 +407,28 @@ class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
fields = ['id', 'name', 'maximum_draw', 'allocated_draw']
|
||||
|
||||
|
||||
class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'name', 'type', 'feed_leg']
|
||||
fields = ['id', 'name', 'feed_leg']
|
||||
|
||||
|
||||
class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
@@ -511,7 +571,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter
|
||||
label='Device model (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=DeviceStatusChoices,
|
||||
choices=DEVICE_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
is_full_depth = django_filters.BooleanFilter(
|
||||
@@ -621,31 +681,12 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site__region',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site__region__in',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region name (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site name (slug)',
|
||||
)
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelChoiceFilter(
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__name',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
@@ -662,10 +703,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
class ConsolePortFilter(DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@@ -678,10 +715,6 @@ class ConsolePortFilter(DeviceComponentFilterSet):
|
||||
|
||||
|
||||
class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@@ -694,10 +727,6 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
|
||||
class PowerPortFilter(DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerPortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@@ -710,10 +739,6 @@ class PowerPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
|
||||
class PowerOutletFilter(DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerOutletTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@@ -733,27 +758,6 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site__region',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site__region__in',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region name (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site__slug',
|
||||
to_field_name='slug',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site name (slug)',
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='name',
|
||||
@@ -789,7 +793,7 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
label='Assigned VID'
|
||||
)
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfaceTypeChoices,
|
||||
choices=IFACE_TYPE_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
@@ -889,6 +893,28 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='device__site__region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='device__site__region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
device_id = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
@@ -926,8 +952,8 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(part_id__icontains=value) |
|
||||
Q(serial__iexact=value) |
|
||||
Q(asset_tag__iexact=value) |
|
||||
Q(serial__icontains=value) |
|
||||
Q(asset_tag__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -938,6 +964,17 @@ class VirtualChassisFilter(django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='master__site__region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='master__site__region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='master__site',
|
||||
queryset=Site.objects.all(),
|
||||
@@ -982,10 +1019,10 @@ class CableFilter(django_filters.FilterSet):
|
||||
label='Search',
|
||||
)
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=CableTypeChoices
|
||||
choices=CABLE_TYPE_CHOICES
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CableStatusChoices
|
||||
choices=CONNECTION_STATUS_CHOICES
|
||||
)
|
||||
color = django_filters.MultipleChoiceFilter(
|
||||
choices=COLOR_CHOICES
|
||||
@@ -993,7 +1030,7 @@ class CableFilter(django_filters.FilterSet):
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueNumberFilter(
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
@@ -1013,6 +1050,14 @@ class CableFilter(django_filters.FilterSet):
|
||||
method='filter_device',
|
||||
field_name='device__site__slug'
|
||||
)
|
||||
tenant_id = MultiValueNumberFilter(
|
||||
method='filter_device',
|
||||
field_name='device__tenant_id'
|
||||
)
|
||||
tenant = MultiValueNumberFilter(
|
||||
method='filter_device',
|
||||
field_name='device__tenant__slug'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
@@ -1036,9 +1081,12 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -1051,11 +1099,11 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
return queryset.filter(connected_endpoint__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(connected_endpoint__device__name__icontains=value)
|
||||
Q(**{'{}__in'.format(name): value}) |
|
||||
Q(**{'connected_endpoint__{}__in'.format(name): value})
|
||||
)
|
||||
|
||||
|
||||
@@ -1064,9 +1112,12 @@ class PowerConnectionFilter(django_filters.FilterSet):
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -1079,11 +1130,11 @@ class PowerConnectionFilter(django_filters.FilterSet):
|
||||
return queryset.filter(_connected_poweroutlet__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(_connected_poweroutlet__device__name__icontains=value)
|
||||
Q(**{'{}__in'.format(name): value}) |
|
||||
Q(**{'_connected_poweroutlet__{}__in'.format(name): value})
|
||||
)
|
||||
|
||||
|
||||
@@ -1092,9 +1143,12 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -1110,11 +1164,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(_connected_interface__device__name__icontains=value)
|
||||
Q(**{'{}__in'.format(name): value}) |
|
||||
Q(**{'_connected_interface__{}__in'.format(name): value})
|
||||
)
|
||||
|
||||
|
||||
@@ -1127,6 +1181,17 @@ class PowerPanelFilter(django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -1165,6 +1230,17 @@ class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='power_panel__site__region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='power_panel__site__region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='power_panel__site',
|
||||
queryset=Site.objects.all(),
|
||||
|
||||
@@ -1910,7 +1910,7 @@
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 1,
|
||||
"face": "front",
|
||||
"face": 0,
|
||||
"status": true,
|
||||
"primary_ip4": 1,
|
||||
"primary_ip6": null,
|
||||
@@ -1931,7 +1931,7 @@
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 17,
|
||||
"face": "rear",
|
||||
"face": 0,
|
||||
"status": true,
|
||||
"primary_ip4": 5,
|
||||
"primary_ip6": null,
|
||||
@@ -1952,7 +1952,7 @@
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 33,
|
||||
"face": "rear",
|
||||
"face": 0,
|
||||
"status": true,
|
||||
"primary_ip4": null,
|
||||
"primary_ip6": null,
|
||||
@@ -1973,7 +1973,7 @@
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 34,
|
||||
"face": "rear",
|
||||
"face": 0,
|
||||
"status": true,
|
||||
"primary_ip4": null,
|
||||
"primary_ip6": null,
|
||||
@@ -1994,7 +1994,7 @@
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 34,
|
||||
"face": "rear",
|
||||
"face": 0,
|
||||
"status": true,
|
||||
"primary_ip4": null,
|
||||
"primary_ip6": null,
|
||||
@@ -2015,7 +2015,7 @@
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 33,
|
||||
"face": "rear",
|
||||
"face": 0,
|
||||
"status": true,
|
||||
"primary_ip4": null,
|
||||
"primary_ip6": null,
|
||||
@@ -2036,7 +2036,7 @@
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 1,
|
||||
"face": "rear",
|
||||
"face": 0,
|
||||
"status": true,
|
||||
"primary_ip4": 3,
|
||||
"primary_ip6": null,
|
||||
@@ -2057,7 +2057,7 @@
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 17,
|
||||
"face": "rear",
|
||||
"face": 0,
|
||||
"status": true,
|
||||
"primary_ip4": 19,
|
||||
"primary_ip6": null,
|
||||
@@ -2078,7 +2078,7 @@
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 42,
|
||||
"face": "rear",
|
||||
"face": 0,
|
||||
"status": true,
|
||||
"primary_ip4": null,
|
||||
"primary_ip6": null,
|
||||
@@ -2099,7 +2099,7 @@
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": null,
|
||||
"face": "",
|
||||
"face": null,
|
||||
"status": true,
|
||||
"primary_ip4": null,
|
||||
"primary_ip6": null,
|
||||
@@ -2120,7 +2120,7 @@
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": null,
|
||||
"face": "",
|
||||
"face": null,
|
||||
"status": true,
|
||||
"primary_ip4": null,
|
||||
"primary_ip6": null,
|
||||
|
||||
195
netbox/dcim/fixtures/initial_data.json
Normal file
195
netbox/dcim/fixtures/initial_data.json
Normal file
@@ -0,0 +1,195 @@
|
||||
[
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Console Server",
|
||||
"slug": "console-server",
|
||||
"color": "009688"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Core Switch",
|
||||
"slug": "core-switch",
|
||||
"color": "2196f3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Distribution Switch",
|
||||
"slug": "distribution-switch",
|
||||
"color": "2196f3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Access Switch",
|
||||
"slug": "access-switch",
|
||||
"color": "2196f3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Management Switch",
|
||||
"slug": "management-switch",
|
||||
"color": "ff9800"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Firewall",
|
||||
"slug": "firewall",
|
||||
"color": "f44336"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Router",
|
||||
"slug": "router",
|
||||
"color": "9c27b0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"name": "Server",
|
||||
"slug": "server",
|
||||
"color": "9e9e9e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"name": "PDU",
|
||||
"slug": "pdu",
|
||||
"color": "607d8b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "APC",
|
||||
"slug": "apc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Cisco",
|
||||
"slug": "cisco"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Dell",
|
||||
"slug": "dell"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "HP",
|
||||
"slug": "hp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Juniper",
|
||||
"slug": "juniper"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Arista",
|
||||
"slug": "arista"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Opengear",
|
||||
"slug": "opengear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"name": "Super Micro",
|
||||
"slug": "super-micro"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Cisco IOS",
|
||||
"slug": "cisco-ios"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Cisco NX-OS",
|
||||
"slug": "cisco-nx-os"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Juniper Junos",
|
||||
"slug": "juniper-junos"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Arista EOS",
|
||||
"slug": "arista-eos"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Linux",
|
||||
"slug": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Opengear",
|
||||
"slug": "opengear"
|
||||
}
|
||||
}
|
||||
]
|
||||
1038
netbox/dcim/forms.py
1038
netbox/dcim/forms.py
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-30 17:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0075_cable_devices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 2.2.6 on 2019-11-06 19:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0076_console_port_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -1,35 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
SITE_STATUS_CHOICES = (
|
||||
(1, 'active'),
|
||||
(2, 'planned'),
|
||||
(4, 'retired'),
|
||||
)
|
||||
|
||||
|
||||
def site_status_to_slug(apps, schema_editor):
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
for id, slug in SITE_STATUS_CHOICES:
|
||||
Site.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0077_power_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# Site.status
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=site_status_to_slug
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,92 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
RACK_TYPE_CHOICES = (
|
||||
(100, '2-post-frame'),
|
||||
(200, '4-post-frame'),
|
||||
(300, '4-post-cabinet'),
|
||||
(1000, 'wall-frame'),
|
||||
(1100, 'wall-cabinet'),
|
||||
)
|
||||
|
||||
RACK_STATUS_CHOICES = (
|
||||
(0, 'reserved'),
|
||||
(1, 'available'),
|
||||
(2, 'planned'),
|
||||
(3, 'active'),
|
||||
(4, 'deprecated'),
|
||||
)
|
||||
|
||||
RACK_DIMENSION_CHOICES = (
|
||||
(1000, 'mm'),
|
||||
(2000, 'in'),
|
||||
)
|
||||
|
||||
|
||||
def rack_type_to_slug(apps, schema_editor):
|
||||
Rack = apps.get_model('dcim', 'Rack')
|
||||
for id, slug in RACK_TYPE_CHOICES:
|
||||
Rack.objects.filter(type=str(id)).update(type=slug)
|
||||
|
||||
|
||||
def rack_status_to_slug(apps, schema_editor):
|
||||
Rack = apps.get_model('dcim', 'Rack')
|
||||
for id, slug in RACK_STATUS_CHOICES:
|
||||
Rack.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
def rack_outer_unit_to_slug(apps, schema_editor):
|
||||
Rack = apps.get_model('dcim', 'Rack')
|
||||
for id, slug in RACK_DIMENSION_CHOICES:
|
||||
Rack.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0078_3569_site_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# Rack.type
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rack_type_to_slug
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
|
||||
# Rack.status
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rack_status_to_slug
|
||||
),
|
||||
|
||||
# Rack.outer_unit
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='outer_unit',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rack_outer_unit_to_slug
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='outer_unit',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,39 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
SUBDEVICE_ROLE_CHOICES = (
|
||||
('true', 'parent'),
|
||||
('false', 'child'),
|
||||
)
|
||||
|
||||
|
||||
def devicetype_subdevicerole_to_slug(apps, schema_editor):
|
||||
DeviceType = apps.get_model('dcim', 'DeviceType')
|
||||
for boolean, slug in SUBDEVICE_ROLE_CHOICES:
|
||||
DeviceType.objects.filter(subdevice_role=boolean).update(subdevice_role=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0079_3569_rack_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# DeviceType.subdevice_role
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=devicetype_subdevicerole_to_slug
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,65 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
DEVICE_FACE_CHOICES = (
|
||||
(0, 'front'),
|
||||
(1, 'rear'),
|
||||
)
|
||||
|
||||
DEVICE_STATUS_CHOICES = (
|
||||
(0, 'offline'),
|
||||
(1, 'active'),
|
||||
(2, 'planned'),
|
||||
(3, 'staged'),
|
||||
(4, 'failed'),
|
||||
(5, 'inventory'),
|
||||
(6, 'decommissioning'),
|
||||
)
|
||||
|
||||
|
||||
def device_face_to_slug(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
for id, slug in DEVICE_FACE_CHOICES:
|
||||
Device.objects.filter(face=str(id)).update(face=slug)
|
||||
|
||||
|
||||
def device_status_to_slug(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
for id, slug in DEVICE_STATUS_CHOICES:
|
||||
Device.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0080_3569_devicetype_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# Device.face
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='face',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=device_face_to_slug
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='face',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
|
||||
# Device.status
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=device_status_to_slug
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,147 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
INTERFACE_TYPE_CHOICES = (
|
||||
(0, 'virtual'),
|
||||
(200, 'lag'),
|
||||
(800, '100base-tx'),
|
||||
(1000, '1000base-t'),
|
||||
(1050, '1000base-x-gbic'),
|
||||
(1100, '1000base-x-sfp'),
|
||||
(1120, '2.5gbase-t'),
|
||||
(1130, '5gbase-t'),
|
||||
(1150, '10gbase-t'),
|
||||
(1170, '10gbase-cx4'),
|
||||
(1200, '10gbase-x-sfpp'),
|
||||
(1300, '10gbase-x-xfp'),
|
||||
(1310, '10gbase-x-xenpak'),
|
||||
(1320, '10gbase-x-x2'),
|
||||
(1350, '25gbase-x-sfp28'),
|
||||
(1400, '40gbase-x-qsfpp'),
|
||||
(1420, '50gbase-x-sfp28'),
|
||||
(1500, '100gbase-x-cfp'),
|
||||
(1510, '100gbase-x-cfp2'),
|
||||
(1520, '100gbase-x-cfp4'),
|
||||
(1550, '100gbase-x-cpak'),
|
||||
(1600, '100gbase-x-qsfp28'),
|
||||
(1650, '200gbase-x-cfp2'),
|
||||
(1700, '200gbase-x-qsfp56'),
|
||||
(1750, '400gbase-x-qsfpdd'),
|
||||
(1800, '400gbase-x-osfp'),
|
||||
(2600, 'ieee802.11a'),
|
||||
(2610, 'ieee802.11g'),
|
||||
(2620, 'ieee802.11n'),
|
||||
(2630, 'ieee802.11ac'),
|
||||
(2640, 'ieee802.11ad'),
|
||||
(2810, 'gsm'),
|
||||
(2820, 'cdma'),
|
||||
(2830, 'lte'),
|
||||
(6100, 'sonet-oc3'),
|
||||
(6200, 'sonet-oc12'),
|
||||
(6300, 'sonet-oc48'),
|
||||
(6400, 'sonet-oc192'),
|
||||
(6500, 'sonet-oc768'),
|
||||
(6600, 'sonet-oc1920'),
|
||||
(6700, 'sonet-oc3840'),
|
||||
(3010, '1gfc-sfp'),
|
||||
(3020, '2gfc-sfp'),
|
||||
(3040, '4gfc-sfp'),
|
||||
(3080, '8gfc-sfpp'),
|
||||
(3160, '16gfc-sfpp'),
|
||||
(3320, '32gfc-sfp28'),
|
||||
(3400, '128gfc-sfp28'),
|
||||
(7010, 'inifiband-sdr'),
|
||||
(7020, 'inifiband-ddr'),
|
||||
(7030, 'inifiband-qdr'),
|
||||
(7040, 'inifiband-fdr10'),
|
||||
(7050, 'inifiband-fdr'),
|
||||
(7060, 'inifiband-edr'),
|
||||
(7070, 'inifiband-hdr'),
|
||||
(7080, 'inifiband-ndr'),
|
||||
(7090, 'inifiband-xdr'),
|
||||
(4000, 't1'),
|
||||
(4010, 'e1'),
|
||||
(4040, 't3'),
|
||||
(4050, 'e3'),
|
||||
(5000, 'cisco-stackwise'),
|
||||
(5050, 'cisco-stackwise-plus'),
|
||||
(5100, 'cisco-flexstack'),
|
||||
(5150, 'cisco-flexstack-plus'),
|
||||
(5200, 'juniper-vcp'),
|
||||
(5300, 'extreme-summitstack'),
|
||||
(5310, 'extreme-summitstack-128'),
|
||||
(5320, 'extreme-summitstack-256'),
|
||||
(5330, 'extreme-summitstack-512'),
|
||||
)
|
||||
|
||||
|
||||
INTERFACE_MODE_CHOICES = (
|
||||
(100, 'access'),
|
||||
(200, 'tagged'),
|
||||
(300, 'tagged-all'),
|
||||
)
|
||||
|
||||
|
||||
def interfacetemplate_type_to_slug(apps, schema_editor):
|
||||
InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate')
|
||||
for id, slug in INTERFACE_TYPE_CHOICES:
|
||||
InterfaceTemplate.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def interface_type_to_slug(apps, schema_editor):
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
for id, slug in INTERFACE_TYPE_CHOICES:
|
||||
Interface.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def interface_mode_to_slug(apps, schema_editor):
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
for id, slug in INTERFACE_MODE_CHOICES:
|
||||
Interface.objects.filter(mode=id).update(mode=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0081_3569_device_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# InterfaceTemplate.type
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=interfacetemplate_type_to_slug
|
||||
),
|
||||
|
||||
# Interface.type
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=interface_type_to_slug
|
||||
),
|
||||
|
||||
# Interface.mode
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mode',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=interface_mode_to_slug
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mode',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,93 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
PORT_TYPE_CHOICES = (
|
||||
(1000, '8p8c'),
|
||||
(1100, '110-punch'),
|
||||
(1200, 'bnc'),
|
||||
(2000, 'st'),
|
||||
(2100, 'sc'),
|
||||
(2110, 'sc-apc'),
|
||||
(2200, 'fc'),
|
||||
(2300, 'lc'),
|
||||
(2310, 'lc-apc'),
|
||||
(2400, 'mtrj'),
|
||||
(2500, 'mpo'),
|
||||
(2600, 'lsh'),
|
||||
(2610, 'lsh-apc'),
|
||||
)
|
||||
|
||||
|
||||
def frontporttemplate_type_to_slug(apps, schema_editor):
|
||||
FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate')
|
||||
for id, slug in PORT_TYPE_CHOICES:
|
||||
FrontPortTemplate.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def rearporttemplate_type_to_slug(apps, schema_editor):
|
||||
RearPortTemplate = apps.get_model('dcim', 'RearPortTemplate')
|
||||
for id, slug in PORT_TYPE_CHOICES:
|
||||
RearPortTemplate.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def frontport_type_to_slug(apps, schema_editor):
|
||||
FrontPort = apps.get_model('dcim', 'FrontPort')
|
||||
for id, slug in PORT_TYPE_CHOICES:
|
||||
FrontPort.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def rearport_type_to_slug(apps, schema_editor):
|
||||
RearPort = apps.get_model('dcim', 'RearPort')
|
||||
for id, slug in PORT_TYPE_CHOICES:
|
||||
RearPort.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0082_3569_interface_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# FrontPortTemplate.type
|
||||
migrations.AlterField(
|
||||
model_name='frontporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=frontporttemplate_type_to_slug
|
||||
),
|
||||
|
||||
# RearPortTemplate.type
|
||||
migrations.AlterField(
|
||||
model_name='rearporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rearporttemplate_type_to_slug
|
||||
),
|
||||
|
||||
# FrontPort.type
|
||||
migrations.AlterField(
|
||||
model_name='frontport',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=frontport_type_to_slug
|
||||
),
|
||||
|
||||
# RearPort.type
|
||||
migrations.AlterField(
|
||||
model_name='rearport',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rearport_type_to_slug
|
||||
),
|
||||
]
|
||||
@@ -1,106 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
CABLE_TYPE_CHOICES = (
|
||||
(1300, 'cat3'),
|
||||
(1500, 'cat5'),
|
||||
(1510, 'cat5e'),
|
||||
(1600, 'cat6'),
|
||||
(1610, 'cat6a'),
|
||||
(1700, 'cat7'),
|
||||
(1800, 'dac-active'),
|
||||
(1810, 'dac-passive'),
|
||||
(1900, 'coaxial'),
|
||||
(3000, 'mmf'),
|
||||
(3010, 'mmf-om1'),
|
||||
(3020, 'mmf-om2'),
|
||||
(3030, 'mmf-om3'),
|
||||
(3040, 'mmf-om4'),
|
||||
(3500, 'smf'),
|
||||
(3510, 'smf-os1'),
|
||||
(3520, 'smf-os2'),
|
||||
(3800, 'aoc'),
|
||||
(5000, 'power'),
|
||||
)
|
||||
|
||||
CABLE_STATUS_CHOICES = (
|
||||
(True, 'connected'),
|
||||
(False, 'planned'),
|
||||
)
|
||||
|
||||
CABLE_LENGTH_UNIT_CHOICES = (
|
||||
(1200, 'm'),
|
||||
(1100, 'cm'),
|
||||
(2100, 'ft'),
|
||||
(2000, 'in'),
|
||||
)
|
||||
|
||||
|
||||
def cable_type_to_slug(apps, schema_editor):
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
for id, slug in CABLE_TYPE_CHOICES:
|
||||
Cable.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def cable_status_to_slug(apps, schema_editor):
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
for bool, slug in CABLE_STATUS_CHOICES:
|
||||
Cable.objects.filter(status=str(bool)).update(status=slug)
|
||||
|
||||
|
||||
def cable_length_unit_to_slug(apps, schema_editor):
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
for id, slug in CABLE_LENGTH_UNIT_CHOICES:
|
||||
Cable.objects.filter(length_unit=id).update(length_unit=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0082_3569_port_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# Cable.type
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=cable_type_to_slug
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
|
||||
# Cable.status
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='status',
|
||||
field=models.CharField(default='connected', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=cable_status_to_slug
|
||||
),
|
||||
|
||||
# Cable.length_unit
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='length_unit',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=cable_length_unit_to_slug
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='length_unit',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,100 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
POWERFEED_STATUS_CHOICES = (
|
||||
(0, 'offline'),
|
||||
(1, 'active'),
|
||||
(2, 'planned'),
|
||||
(4, 'failed'),
|
||||
)
|
||||
|
||||
POWERFEED_TYPE_CHOICES = (
|
||||
(1, 'primary'),
|
||||
(2, 'redundant'),
|
||||
)
|
||||
|
||||
POWERFEED_SUPPLY_CHOICES = (
|
||||
(1, 'ac'),
|
||||
(2, 'dc'),
|
||||
)
|
||||
|
||||
POWERFEED_PHASE_CHOICES = (
|
||||
(1, 'single-phase'),
|
||||
(3, 'three-phase'),
|
||||
)
|
||||
|
||||
|
||||
def powerfeed_status_to_slug(apps, schema_editor):
|
||||
PowerFeed = apps.get_model('dcim', 'PowerFeed')
|
||||
for id, slug in POWERFEED_STATUS_CHOICES:
|
||||
PowerFeed.objects.filter(status=id).update(status=slug)
|
||||
|
||||
|
||||
def powerfeed_type_to_slug(apps, schema_editor):
|
||||
PowerFeed = apps.get_model('dcim', 'PowerFeed')
|
||||
for id, slug in POWERFEED_TYPE_CHOICES:
|
||||
PowerFeed.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def powerfeed_supply_to_slug(apps, schema_editor):
|
||||
PowerFeed = apps.get_model('dcim', 'PowerFeed')
|
||||
for id, slug in POWERFEED_SUPPLY_CHOICES:
|
||||
PowerFeed.objects.filter(supply=id).update(supply=slug)
|
||||
|
||||
|
||||
def powerfeed_phase_to_slug(apps, schema_editor):
|
||||
PowerFeed = apps.get_model('dcim', 'PowerFeed')
|
||||
for id, slug in POWERFEED_PHASE_CHOICES:
|
||||
PowerFeed.objects.filter(phase=id).update(phase=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0083_3569_cable_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# PowerFeed.status
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=powerfeed_status_to_slug
|
||||
),
|
||||
|
||||
# PowerFeed.type
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='type',
|
||||
field=models.CharField(default='primary', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=powerfeed_type_to_slug
|
||||
),
|
||||
|
||||
# PowerFeed.supply
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='supply',
|
||||
field=models.CharField(default='ac', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=powerfeed_supply_to_slug
|
||||
),
|
||||
|
||||
# PowerFeed.phase
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='phase',
|
||||
field=models.CharField(default='single-phase', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=powerfeed_phase_to_slug
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,62 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
POWEROUTLET_FEED_LEG_CHOICES_CHOICES = (
|
||||
(1, 'A'),
|
||||
(2, 'B'),
|
||||
(3, 'C'),
|
||||
)
|
||||
|
||||
|
||||
def poweroutlettemplate_feed_leg_to_slug(apps, schema_editor):
|
||||
PowerOutletTemplate = apps.get_model('dcim', 'PowerOutletTemplate')
|
||||
for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
|
||||
PowerOutletTemplate.objects.filter(feed_leg=id).update(feed_leg=slug)
|
||||
|
||||
|
||||
def poweroutlet_feed_leg_to_slug(apps, schema_editor):
|
||||
PowerOutlet = apps.get_model('dcim', 'PowerOutlet')
|
||||
for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
|
||||
PowerOutlet.objects.filter(feed_leg=id).update(feed_leg=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0084_3569_powerfeed_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# PowerOutletTemplate.feed_leg
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='feed_leg',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=poweroutlettemplate_feed_leg_to_slug
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='feed_leg',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
|
||||
# PowerOutlet.feed_leg
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='feed_leg',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=poweroutlet_feed_leg_to_slug
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='feed_leg',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 2.2.6 on 2019-12-09 15:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0006_custom_tag_models'),
|
||||
('dcim', '0085_3569_poweroutlet_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=64, null=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together={('rack', 'position', 'face'), ('virtual_chassis', 'vc_position'), ('site', 'tenant', 'name')},
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 2.2.6 on 2019-12-10 17:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0086_device_name_nonunique'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackrole',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-12 02:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0087_role_descriptions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='available_power',
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -156,6 +156,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 %}
|
||||
"""
|
||||
|
||||
DEVICETYPE_INSTANCES_TEMPLATE = """
|
||||
<a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
|
||||
"""
|
||||
@@ -272,17 +276,16 @@ class RackGroupTable(BaseTable):
|
||||
|
||||
class RackRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
rack_count = tables.Column(verbose_name='Racks')
|
||||
color = tables.TemplateColumn(COLOR_LABEL)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackRole
|
||||
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -390,6 +393,10 @@ class DeviceTypeTable(BaseTable):
|
||||
verbose_name='Device Type'
|
||||
)
|
||||
is_full_depth = BooleanColumn(verbose_name='Full Depth')
|
||||
subdevice_role = tables.TemplateColumn(
|
||||
template_code=SUBDEVICE_ROLE_TEMPLATE,
|
||||
verbose_name='Subdevice Role'
|
||||
)
|
||||
instance_count = tables.TemplateColumn(
|
||||
template_code=DEVICETYPE_INSTANCES_TEMPLATE,
|
||||
verbose_name='Instances'
|
||||
@@ -417,19 +424,10 @@ class ConsolePortTemplateTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePortTemplate
|
||||
fields = ('pk', 'name', 'type', 'actions')
|
||||
fields = ('pk', 'name', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class ConsolePortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'description')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
actions = tables.TemplateColumn(
|
||||
@@ -444,15 +442,6 @@ class ConsoleServerPortTemplateTable(BaseTable):
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class ConsoleServerPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = ('device', 'name', 'description')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class PowerPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
actions = tables.TemplateColumn(
|
||||
@@ -463,19 +452,10 @@ class PowerPortTemplateTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPortTemplate
|
||||
fields = ('pk', 'name', 'type', 'maximum_draw', 'allocated_draw', 'actions')
|
||||
fields = ('pk', 'name', 'maximum_draw', 'allocated_draw', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class PowerPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class PowerOutletTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
actions = tables.TemplateColumn(
|
||||
@@ -486,19 +466,10 @@ class PowerOutletTemplateTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutletTemplate
|
||||
fields = ('pk', 'name', 'type', 'power_port', 'feed_leg', 'actions')
|
||||
fields = ('pk', 'name', 'power_port', 'feed_leg', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class PowerOutletImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('device', 'name', 'description', 'power_port', 'feed_leg')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class InterfaceTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
|
||||
@@ -514,16 +485,6 @@ class InterfaceTemplateTable(BaseTable):
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class InterfaceImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class FrontPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
rear_port_position = tables.Column(
|
||||
@@ -541,15 +502,6 @@ class FrontPortTemplateTable(BaseTable):
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class FrontPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPort
|
||||
fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class RearPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
actions = tables.TemplateColumn(
|
||||
@@ -564,15 +516,6 @@ class RearPortTemplateTable(BaseTable):
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class RearPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPort
|
||||
fields = ('device', 'name', 'description', 'type', 'position')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class DeviceBayTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
actions = tables.TemplateColumn(
|
||||
@@ -615,7 +558,7 @@ class DeviceRoleTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
@@ -702,28 +645,11 @@ class DeviceImportTable(BaseTable):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceComponentDetailTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
cable = tables.LinkColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
order_by = ('device', 'name')
|
||||
fields = ('pk', 'device', 'name', 'type', 'description', 'cable')
|
||||
sequence = ('pk', 'device', 'name', 'type', 'description', 'cable')
|
||||
|
||||
|
||||
class ConsolePortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('name', 'type')
|
||||
|
||||
|
||||
class ConsolePortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
|
||||
pass
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class ConsoleServerPortTable(BaseTable):
|
||||
@@ -733,39 +659,18 @@ class ConsoleServerPortTable(BaseTable):
|
||||
fields = ('name', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class PowerPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('name', 'type')
|
||||
|
||||
|
||||
class PowerPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
|
||||
pass
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class PowerOutletTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('name', 'type', 'description')
|
||||
|
||||
|
||||
class PowerOutletDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
|
||||
pass
|
||||
fields = ('name', 'description')
|
||||
|
||||
|
||||
class InterfaceTable(BaseTable):
|
||||
@@ -775,15 +680,6 @@ class InterfaceTable(BaseTable):
|
||||
fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
class InterfaceDetailTable(DeviceComponentDetailTable):
|
||||
parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
|
||||
|
||||
class Meta(InterfaceTable.Meta):
|
||||
order_by = ('parent', 'name')
|
||||
fields = ('pk', 'parent', 'name', 'type', 'description', 'cable')
|
||||
sequence = ('pk', 'parent', 'name', 'type', 'description', 'cable')
|
||||
|
||||
|
||||
class FrontPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -792,13 +688,6 @@ class FrontPortTable(BaseTable):
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class FrontPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class RearPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -807,13 +696,6 @@ class RearPortTable(BaseTable):
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class RearPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class DeviceBayTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -821,26 +703,6 @@ class DeviceBayTable(BaseTable):
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class DeviceBayDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
installed_device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceBayTable.Meta):
|
||||
fields = ('pk', 'name', 'device', 'installed_device')
|
||||
sequence = ('pk', 'name', 'device', 'installed_device')
|
||||
exclude = ('cable',)
|
||||
|
||||
|
||||
class DeviceBayImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('device', 'name', 'installed_device', 'description')
|
||||
empty_text = False
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
@@ -13,7 +11,7 @@ from dcim.models import (
|
||||
Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
|
||||
)
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from extras.models import Graph
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from utilities.testing import APITestCase
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
|
||||
@@ -140,20 +138,16 @@ class SiteTest(APITestCase):
|
||||
|
||||
def test_get_site_graphs(self):
|
||||
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
self.graph1 = Graph.objects.create(
|
||||
type=site_ct,
|
||||
name='Test Graph 1',
|
||||
type=GRAPH_TYPE_SITE, name='Test Graph 1',
|
||||
source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1'
|
||||
)
|
||||
self.graph2 = Graph.objects.create(
|
||||
type=site_ct,
|
||||
name='Test Graph 2',
|
||||
type=GRAPH_TYPE_SITE, name='Test Graph 2',
|
||||
source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2'
|
||||
)
|
||||
self.graph3 = Graph.objects.create(
|
||||
type=site_ct,
|
||||
name='Test Graph 3',
|
||||
type=GRAPH_TYPE_SITE, name='Test Graph 3',
|
||||
source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3'
|
||||
)
|
||||
|
||||
@@ -186,7 +180,7 @@ class SiteTest(APITestCase):
|
||||
'name': 'Test Site 4',
|
||||
'slug': 'test-site-4',
|
||||
'region': self.region1.pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
'status': SITE_STATUS_ACTIVE,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-list')
|
||||
@@ -206,19 +200,19 @@ class SiteTest(APITestCase):
|
||||
'name': 'Test Site 4',
|
||||
'slug': 'test-site-4',
|
||||
'region': self.region1.pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
'status': SITE_STATUS_ACTIVE,
|
||||
},
|
||||
{
|
||||
'name': 'Test Site 5',
|
||||
'slug': 'test-site-5',
|
||||
'region': self.region1.pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
'status': SITE_STATUS_ACTIVE,
|
||||
},
|
||||
{
|
||||
'name': 'Test Site 6',
|
||||
'slug': 'test-site-6',
|
||||
'region': self.region1.pk,
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
'status': SITE_STATUS_ACTIVE,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -2422,20 +2416,16 @@ class InterfaceTest(APITestCase):
|
||||
|
||||
def test_get_interface_graphs(self):
|
||||
|
||||
interface_ct = ContentType.objects.get_for_model(Interface)
|
||||
self.graph1 = Graph.objects.create(
|
||||
type=interface_ct,
|
||||
name='Test Graph 1',
|
||||
type=GRAPH_TYPE_INTERFACE, name='Test Graph 1',
|
||||
source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'
|
||||
)
|
||||
self.graph2 = Graph.objects.create(
|
||||
type=interface_ct,
|
||||
name='Test Graph 2',
|
||||
type=GRAPH_TYPE_INTERFACE, name='Test Graph 2',
|
||||
source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'
|
||||
)
|
||||
self.graph3 = Graph.objects.create(
|
||||
type=interface_ct,
|
||||
name='Test Graph 3',
|
||||
type=GRAPH_TYPE_INTERFACE, name='Test Graph 3',
|
||||
source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'
|
||||
)
|
||||
|
||||
@@ -2483,7 +2473,7 @@ class InterfaceTest(APITestCase):
|
||||
data = {
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 4',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'mode': IFACE_MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan3.id,
|
||||
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
|
||||
}
|
||||
@@ -2530,21 +2520,21 @@ class InterfaceTest(APITestCase):
|
||||
{
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 4',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'mode': IFACE_MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan2.id,
|
||||
'tagged_vlans': [self.vlan1.id],
|
||||
},
|
||||
{
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 5',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'mode': IFACE_MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan2.id,
|
||||
'tagged_vlans': [self.vlan1.id],
|
||||
},
|
||||
{
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 6',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'mode': IFACE_MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan2.id,
|
||||
'tagged_vlans': [self.vlan1.id],
|
||||
},
|
||||
@@ -2563,7 +2553,7 @@ class InterfaceTest(APITestCase):
|
||||
def test_update_interface(self):
|
||||
|
||||
lag_interface = Interface.objects.create(
|
||||
device=self.device, name='Test LAG Interface', type=InterfaceTypeChoices.TYPE_LAG
|
||||
device=self.device, name='Test LAG Interface', type=IFACE_TYPE_LAG
|
||||
)
|
||||
|
||||
data = {
|
||||
@@ -2600,11 +2590,11 @@ class DeviceBayTest(APITestCase):
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype1 = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type',
|
||||
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
|
||||
subdevice_role=SUBDEVICE_ROLE_PARENT
|
||||
)
|
||||
self.devicetype2 = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Child Device Type', slug='child-device-type',
|
||||
subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
|
||||
subdevice_role=SUBDEVICE_ROLE_CHILD
|
||||
)
|
||||
devicerole = DeviceRole.objects.create(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
@@ -2851,7 +2841,7 @@ class CableTest(APITestCase):
|
||||
)
|
||||
for device in [self.device1, self.device2]:
|
||||
for i in range(0, 10):
|
||||
Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth{}'.format(i)).save()
|
||||
Interface(device=device, type=IFACE_TYPE_1GE_FIXED, name='eth{}'.format(i)).save()
|
||||
|
||||
self.cable1 = Cable(
|
||||
termination_a=self.device1.interfaces.get(name='eth0'),
|
||||
@@ -2895,7 +2885,7 @@ class CableTest(APITestCase):
|
||||
'termination_a_id': interface_a.pk,
|
||||
'termination_b_type': 'dcim.interface',
|
||||
'termination_b_id': interface_b.pk,
|
||||
'status': CableStatusChoices.STATUS_PLANNED,
|
||||
'status': CONNECTION_STATUS_PLANNED,
|
||||
'label': 'Test Cable 4',
|
||||
}
|
||||
|
||||
@@ -2949,7 +2939,7 @@ class CableTest(APITestCase):
|
||||
|
||||
data = {
|
||||
'label': 'Test Cable X',
|
||||
'status': CableStatusChoices.STATUS_CONNECTED,
|
||||
'status': CONNECTION_STATUS_CONNECTED,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:cable-detail', kwargs={'pk': self.cable1.pk})
|
||||
@@ -3043,16 +3033,16 @@ class ConnectionTest(APITestCase):
|
||||
device=self.device2, name='Test Console Server Port 1'
|
||||
)
|
||||
rearport1 = RearPort.objects.create(
|
||||
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
|
||||
device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
|
||||
)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
|
||||
device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
|
||||
)
|
||||
rearport2 = RearPort.objects.create(
|
||||
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
|
||||
device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
|
||||
)
|
||||
frontport2 = FrontPort.objects.create(
|
||||
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
|
||||
device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
|
||||
)
|
||||
|
||||
url = reverse('dcim-api:cable-list')
|
||||
@@ -3171,16 +3161,16 @@ class ConnectionTest(APITestCase):
|
||||
device=self.device2, name='Test Interface 2'
|
||||
)
|
||||
rearport1 = RearPort.objects.create(
|
||||
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
|
||||
device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
|
||||
)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
|
||||
device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
|
||||
)
|
||||
rearport2 = RearPort.objects.create(
|
||||
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
|
||||
device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
|
||||
)
|
||||
frontport2 = FrontPort.objects.create(
|
||||
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
|
||||
device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
|
||||
)
|
||||
|
||||
url = reverse('dcim-api:cable-list')
|
||||
@@ -3282,16 +3272,16 @@ class ConnectionTest(APITestCase):
|
||||
circuit=circuit, term_side='A', site=self.site, port_speed=10000
|
||||
)
|
||||
rearport1 = RearPort.objects.create(
|
||||
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
|
||||
device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
|
||||
)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
|
||||
device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
|
||||
)
|
||||
rearport2 = RearPort.objects.create(
|
||||
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
|
||||
device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
|
||||
)
|
||||
frontport2 = FrontPort.objects.create(
|
||||
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
|
||||
device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
|
||||
)
|
||||
|
||||
url = reverse('dcim-api:cable-list')
|
||||
@@ -3420,23 +3410,23 @@ class VirtualChassisTest(APITestCase):
|
||||
device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
|
||||
)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
|
||||
Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
|
||||
Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
|
||||
Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
|
||||
Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
|
||||
Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
|
||||
Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
|
||||
Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
|
||||
Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
|
||||
for i in range(0, 13):
|
||||
Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED)
|
||||
Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
|
||||
|
||||
# Create two VirtualChassis with three members each
|
||||
self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1')
|
||||
@@ -3688,22 +3678,22 @@ class PowerFeedTest(APITestCase):
|
||||
site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2'
|
||||
)
|
||||
self.powerfeed1 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=PowerFeedTypeChoices.TYPE_PRIMARY
|
||||
power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=POWERFEED_TYPE_PRIMARY
|
||||
)
|
||||
self.powerfeed2 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
|
||||
power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=POWERFEED_TYPE_REDUNDANT
|
||||
)
|
||||
self.powerfeed3 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=PowerFeedTypeChoices.TYPE_PRIMARY
|
||||
power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=POWERFEED_TYPE_PRIMARY
|
||||
)
|
||||
self.powerfeed4 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
|
||||
power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=POWERFEED_TYPE_REDUNDANT
|
||||
)
|
||||
self.powerfeed5 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=PowerFeedTypeChoices.TYPE_PRIMARY
|
||||
power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=POWERFEED_TYPE_PRIMARY
|
||||
)
|
||||
self.powerfeed6 = PowerFeed.objects.create(
|
||||
power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
|
||||
power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=POWERFEED_TYPE_REDUNDANT
|
||||
)
|
||||
|
||||
def test_get_powerfeed(self):
|
||||
@@ -3736,7 +3726,7 @@ class PowerFeedTest(APITestCase):
|
||||
'name': 'Test Power Feed 4A',
|
||||
'power_panel': self.powerpanel1.pk,
|
||||
'rack': self.rack4.pk,
|
||||
'type': PowerFeedTypeChoices.TYPE_PRIMARY,
|
||||
'type': POWERFEED_TYPE_PRIMARY,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:powerfeed-list')
|
||||
@@ -3756,13 +3746,13 @@ class PowerFeedTest(APITestCase):
|
||||
'name': 'Test Power Feed 4A',
|
||||
'power_panel': self.powerpanel1.pk,
|
||||
'rack': self.rack4.pk,
|
||||
'type': PowerFeedTypeChoices.TYPE_PRIMARY,
|
||||
'type': POWERFEED_TYPE_PRIMARY,
|
||||
},
|
||||
{
|
||||
'name': 'Test Power Feed 4B',
|
||||
'power_panel': self.powerpanel1.pk,
|
||||
'rack': self.rack4.pk,
|
||||
'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
|
||||
'type': POWERFEED_TYPE_REDUNDANT,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3779,7 +3769,7 @@ class PowerFeedTest(APITestCase):
|
||||
data = {
|
||||
'name': 'Test Power Feed X',
|
||||
'rack': self.rack4.pk,
|
||||
'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
|
||||
'type': POWERFEED_TYPE_REDUNDANT,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
|
||||
|
||||
2401
netbox/dcim/tests/test_filters.py
Normal file
2401
netbox/dcim/tests/test_filters.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,10 +21,10 @@ class DeviceTestCase(TestCase):
|
||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
||||
'site': get_id(Site, 'test1'),
|
||||
'rack': '1',
|
||||
'face': DeviceFaceChoices.FACE_FRONT,
|
||||
'face': RACK_FACE_FRONT,
|
||||
'position': 41,
|
||||
'platform': get_id(Platform, 'juniper-junos'),
|
||||
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||
'status': DEVICE_STATUS_ACTIVE,
|
||||
})
|
||||
self.assertTrue(test.is_valid(), test.fields['position'].choices)
|
||||
self.assertTrue(test.save())
|
||||
@@ -38,10 +38,10 @@ class DeviceTestCase(TestCase):
|
||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
||||
'site': get_id(Site, 'test1'),
|
||||
'rack': '1',
|
||||
'face': DeviceFaceChoices.FACE_FRONT,
|
||||
'face': RACK_FACE_FRONT,
|
||||
'position': 1,
|
||||
'platform': get_id(Platform, 'juniper-junos'),
|
||||
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||
'status': DEVICE_STATUS_ACTIVE,
|
||||
})
|
||||
self.assertFalse(test.is_valid())
|
||||
|
||||
@@ -54,10 +54,10 @@ class DeviceTestCase(TestCase):
|
||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
||||
'site': get_id(Site, 'test1'),
|
||||
'rack': '1',
|
||||
'face': '',
|
||||
'face': None,
|
||||
'position': None,
|
||||
'platform': None,
|
||||
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||
'status': DEVICE_STATUS_ACTIVE,
|
||||
})
|
||||
self.assertTrue(test.is_valid())
|
||||
self.assertTrue(test.save())
|
||||
@@ -71,10 +71,10 @@ class DeviceTestCase(TestCase):
|
||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
||||
'site': get_id(Site, 'test1'),
|
||||
'rack': '1',
|
||||
'face': DeviceFaceChoices.FACE_REAR,
|
||||
'face': RACK_FACE_REAR,
|
||||
'position': None,
|
||||
'platform': None,
|
||||
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||
'status': DEVICE_STATUS_ACTIVE,
|
||||
})
|
||||
self.assertTrue(test.is_valid())
|
||||
self.assertTrue(test.save())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import *
|
||||
from tenancy.models import Tenant
|
||||
|
||||
|
||||
class RackTestCase(TestCase):
|
||||
@@ -88,7 +87,7 @@ class RackTestCase(TestCase):
|
||||
site=self.site1,
|
||||
rack=rack1,
|
||||
position=43,
|
||||
face=DeviceFaceChoices.FACE_FRONT,
|
||||
face=RACK_FACE_FRONT,
|
||||
)
|
||||
device1.save()
|
||||
|
||||
@@ -118,7 +117,7 @@ class RackTestCase(TestCase):
|
||||
site=self.site1,
|
||||
rack=self.rack,
|
||||
position=10,
|
||||
face=DeviceFaceChoices.FACE_REAR,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
device1.save()
|
||||
|
||||
@@ -126,14 +125,14 @@ class RackTestCase(TestCase):
|
||||
self.assertEqual(list(self.rack.units), list(reversed(range(1, 43))))
|
||||
|
||||
# Validate inventory (front face)
|
||||
rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT)
|
||||
rack1_inventory_front = self.rack.get_front_elevation()
|
||||
self.assertEqual(rack1_inventory_front[-10]['device'], device1)
|
||||
del(rack1_inventory_front[-10])
|
||||
for u in rack1_inventory_front:
|
||||
self.assertIsNone(u['device'])
|
||||
|
||||
# Validate inventory (rear face)
|
||||
rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR)
|
||||
rack1_inventory_rear = self.rack.get_rear_elevation()
|
||||
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
|
||||
del(rack1_inventory_rear[-10])
|
||||
for u in rack1_inventory_rear:
|
||||
@@ -147,7 +146,7 @@ class RackTestCase(TestCase):
|
||||
site=self.site1,
|
||||
rack=self.rack,
|
||||
position=None,
|
||||
face='',
|
||||
face=None,
|
||||
)
|
||||
self.assertTrue(pdu)
|
||||
|
||||
@@ -188,20 +187,20 @@ class DeviceTestCase(TestCase):
|
||||
device_type=self.device_type,
|
||||
name='Power Outlet 1',
|
||||
power_port=ppt,
|
||||
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
|
||||
feed_leg=POWERFEED_LEG_A
|
||||
).save()
|
||||
|
||||
InterfaceTemplate(
|
||||
device_type=self.device_type,
|
||||
name='Interface 1',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
||||
type=IFACE_TYPE_1GE_FIXED,
|
||||
mgmt_only=True
|
||||
).save()
|
||||
|
||||
rpt = RearPortTemplate(
|
||||
device_type=self.device_type,
|
||||
name='Rear Port 1',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
type=PORT_TYPE_8P8C,
|
||||
positions=8
|
||||
)
|
||||
rpt.save()
|
||||
@@ -209,7 +208,7 @@ class DeviceTestCase(TestCase):
|
||||
FrontPortTemplate(
|
||||
device_type=self.device_type,
|
||||
name='Front Port 1',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
type=PORT_TYPE_8P8C,
|
||||
rear_port=rpt,
|
||||
rear_port_position=2
|
||||
).save()
|
||||
@@ -252,27 +251,27 @@ class DeviceTestCase(TestCase):
|
||||
device=d,
|
||||
name='Power Outlet 1',
|
||||
power_port=pp,
|
||||
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
|
||||
feed_leg=POWERFEED_LEG_A
|
||||
)
|
||||
|
||||
Interface.objects.get(
|
||||
device=d,
|
||||
name='Interface 1',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
||||
type=IFACE_TYPE_1GE_FIXED,
|
||||
mgmt_only=True
|
||||
)
|
||||
|
||||
rp = RearPort.objects.get(
|
||||
device=d,
|
||||
name='Rear Port 1',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
type=PORT_TYPE_8P8C,
|
||||
positions=8
|
||||
)
|
||||
|
||||
FrontPort.objects.get(
|
||||
device=d,
|
||||
name='Front Port 1',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
type=PORT_TYPE_8P8C,
|
||||
rear_port=rp,
|
||||
rear_port_position=2
|
||||
)
|
||||
@@ -282,42 +281,6 @@ class DeviceTestCase(TestCase):
|
||||
name='Device Bay 1'
|
||||
)
|
||||
|
||||
def test_device_duplicate_name_per_site(self):
|
||||
|
||||
device1 = Device(
|
||||
site=self.site,
|
||||
device_type=self.device_type,
|
||||
device_role=self.device_role,
|
||||
name='Test Device 1'
|
||||
)
|
||||
device1.save()
|
||||
|
||||
device2 = Device(
|
||||
site=device1.site,
|
||||
device_type=device1.device_type,
|
||||
device_role=device1.device_role,
|
||||
name=device1.name
|
||||
)
|
||||
|
||||
# Two devices assigned to the same Site and no Tenant should fail validation
|
||||
with self.assertRaises(ValidationError):
|
||||
device2.full_clean()
|
||||
|
||||
tenant = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
|
||||
device1.tenant = tenant
|
||||
device1.save()
|
||||
device2.tenant = tenant
|
||||
|
||||
# Two devices assigned to the same Site and the same Tenant should fail validation
|
||||
with self.assertRaises(ValidationError):
|
||||
device2.full_clean()
|
||||
|
||||
device2.tenant = None
|
||||
|
||||
# Two devices assigned to the same Site and different Tenants should pass validation
|
||||
device2.full_clean()
|
||||
device2.save()
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
|
||||
@@ -362,9 +325,12 @@ class CableTestCase(TestCase):
|
||||
|
||||
def test_cable_deletion(self):
|
||||
"""
|
||||
When a Cable is deleted, the `cable` field on its termination points must be nullified.
|
||||
When a Cable is deleted, the `cable` field on its termination points must be nullified. The str() method
|
||||
should still return the PK of the string even after being nullified.
|
||||
"""
|
||||
self.cable.delete()
|
||||
self.assertIsNone(self.cable.pk)
|
||||
self.assertNotEqual(str(self.cable), '#None')
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.cable)
|
||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||
@@ -416,7 +382,7 @@ class CableTestCase(TestCase):
|
||||
"""
|
||||
A cable cannot terminate to a virtual interface
|
||||
"""
|
||||
virtual_interface = Interface(device=self.device1, name="V1", type=InterfaceTypeChoices.TYPE_VIRTUAL)
|
||||
virtual_interface = Interface(device=self.device1, name="V1", type=IFACE_TYPE_VIRTUAL)
|
||||
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
@@ -425,7 +391,7 @@ class CableTestCase(TestCase):
|
||||
"""
|
||||
A cable cannot terminate to a wireless interface
|
||||
"""
|
||||
wireless_interface = Interface(device=self.device1, name="W1", type=InterfaceTypeChoices.TYPE_80211A)
|
||||
wireless_interface = Interface(device=self.device1, name="W1", type=IFACE_TYPE_80211A)
|
||||
cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
@@ -458,16 +424,16 @@ class CablePathTestCase(TestCase):
|
||||
device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
|
||||
)
|
||||
self.rear_port1 = RearPort.objects.create(
|
||||
device=self.panel1, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C
|
||||
device=self.panel1, name='Rear Port 1', type=PORT_TYPE_8P8C
|
||||
)
|
||||
self.front_port1 = FrontPort.objects.create(
|
||||
device=self.panel1, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port1
|
||||
device=self.panel1, name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=self.rear_port1
|
||||
)
|
||||
self.rear_port2 = RearPort.objects.create(
|
||||
device=self.panel2, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C
|
||||
device=self.panel2, name='Rear Port 2', type=PORT_TYPE_8P8C
|
||||
)
|
||||
self.front_port2 = FrontPort.objects.create(
|
||||
device=self.panel2, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port2
|
||||
device=self.panel2, name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=self.rear_port2
|
||||
)
|
||||
|
||||
def test_path_completion(self):
|
||||
@@ -487,11 +453,7 @@ class CablePathTestCase(TestCase):
|
||||
self.assertIsNone(interface1.connection_status)
|
||||
|
||||
# Third segment
|
||||
cable3 = Cable(
|
||||
termination_a=self.front_port2,
|
||||
termination_b=self.interface2,
|
||||
status=CableStatusChoices.STATUS_PLANNED
|
||||
)
|
||||
cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2, status=CONNECTION_STATUS_PLANNED)
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,7 +82,7 @@ urlpatterns = [
|
||||
# Device types
|
||||
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
|
||||
path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
|
||||
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||
@@ -171,58 +171,49 @@ urlpatterns = [
|
||||
path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||
path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
path(r'console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
||||
path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
|
||||
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
|
||||
|
||||
# Console server ports
|
||||
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
||||
path(r'devices/<int:pk>/console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
|
||||
path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
path(r'console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'),
|
||||
path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
|
||||
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
|
||||
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
|
||||
|
||||
# Power ports
|
||||
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||
path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
path(r'power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
|
||||
path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
|
||||
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
|
||||
|
||||
# Power outlets
|
||||
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
||||
path(r'devices/<int:pk>/power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
|
||||
path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
path(r'power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'),
|
||||
path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
|
||||
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
|
||||
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
|
||||
|
||||
# Interfaces
|
||||
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||
path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'),
|
||||
path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
|
||||
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
@@ -231,47 +222,40 @@ urlpatterns = [
|
||||
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
|
||||
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
|
||||
|
||||
# Front ports
|
||||
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||
path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
|
||||
path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
|
||||
path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
|
||||
path(r'front-ports/', views.FrontPortListView.as_view(), name='frontport_list'),
|
||||
path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
|
||||
path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||
path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
|
||||
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
|
||||
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
|
||||
path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
|
||||
|
||||
# Rear ports
|
||||
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
|
||||
path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
|
||||
path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
|
||||
path(r'rear-ports/', views.RearPortListView.as_view(), name='rearport_list'),
|
||||
path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
|
||||
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
|
||||
path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
|
||||
|
||||
# Device bays
|
||||
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
|
||||
path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
path(r'device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
|
||||
path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
|
||||
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
|
||||
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
|
||||
path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
|
||||
|
||||
# Inventory items
|
||||
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from collections import OrderedDict
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
@@ -17,7 +16,8 @@ from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.models import Graph
|
||||
from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.models import Graph, TopologyMap
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import Prefix, VLAN
|
||||
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
||||
@@ -26,7 +26,7 @@ from utilities.paginator import EnhancedPaginator
|
||||
from utilities.utils import csv_format
|
||||
from utilities.views import (
|
||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
|
||||
ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
@@ -208,12 +208,14 @@ class SiteView(PermissionRequiredMixin, View):
|
||||
'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
|
||||
}
|
||||
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
||||
show_graphs = Graph.objects.filter(type__model='site').exists()
|
||||
topology_maps = TopologyMap.objects.filter(site=site)
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
|
||||
|
||||
return render(request, 'dcim/site.html', {
|
||||
'site': site,
|
||||
'stats': stats,
|
||||
'rack_groups': rack_groups,
|
||||
'topology_maps': topology_maps,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
|
||||
@@ -386,7 +388,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
|
||||
'page': page,
|
||||
'total_count': total_count,
|
||||
'face_id': face_id,
|
||||
'filter_form': forms.RackFilterForm(request.GET),
|
||||
'filter_form': forms.RackElevationFilterForm(request.GET),
|
||||
})
|
||||
|
||||
|
||||
@@ -419,6 +421,8 @@ class RackView(PermissionRequiredMixin, View):
|
||||
'nonracked_devices': nonracked_devices,
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
'front_elevation': rack.get_front_elevation(),
|
||||
'rear_elevation': rack.get_rear_elevation(),
|
||||
})
|
||||
|
||||
|
||||
@@ -655,31 +659,11 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
default_return_url = 'dcim:devicetype_list'
|
||||
|
||||
|
||||
class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView):
|
||||
permission_required = [
|
||||
'dcim.add_devicetype',
|
||||
'dcim.add_consoleporttemplate',
|
||||
'dcim.add_consoleserverporttemplate',
|
||||
'dcim.add_powerporttemplate',
|
||||
'dcim.add_poweroutlettemplate',
|
||||
'dcim.add_interfacetemplate',
|
||||
'dcim.add_frontporttemplate',
|
||||
'dcim.add_rearporttemplate',
|
||||
'dcim.add_devicebaytemplate',
|
||||
]
|
||||
model = DeviceType
|
||||
model_form = forms.DeviceTypeImportForm
|
||||
related_object_forms = OrderedDict((
|
||||
('console-ports', forms.ConsolePortTemplateImportForm),
|
||||
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
|
||||
('power-ports', forms.PowerPortTemplateImportForm),
|
||||
('power-outlets', forms.PowerOutletTemplateImportForm),
|
||||
('interfaces', forms.InterfaceTemplateImportForm),
|
||||
('rear-ports', forms.RearPortTemplateImportForm),
|
||||
('front-ports', forms.FrontPortTemplateImportForm),
|
||||
('device-bays', forms.DeviceBayTemplateImportForm),
|
||||
))
|
||||
default_return_url = 'dcim:devicetype_import'
|
||||
class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_devicetype'
|
||||
model_form = forms.DeviceTypeCSVForm
|
||||
table = tables.DeviceTypeTable
|
||||
default_return_url = 'dcim:devicetype_list'
|
||||
|
||||
|
||||
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
@@ -1055,8 +1039,8 @@ class DeviceView(PermissionRequiredMixin, View):
|
||||
'secrets': secrets,
|
||||
'vc_members': vc_members,
|
||||
'related_devices': related_devices,
|
||||
'show_graphs': Graph.objects.filter(type__model='device').exists(),
|
||||
'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(),
|
||||
'show_graphs': Graph.objects.filter(type=GRAPH_TYPE_DEVICE).exists(),
|
||||
'show_interface_graphs': Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists(),
|
||||
})
|
||||
|
||||
|
||||
@@ -1194,15 +1178,6 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_consoleport'
|
||||
queryset = ConsolePort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
|
||||
filter = filters.ConsolePortFilter
|
||||
filter_form = forms.ConsolePortFilterForm
|
||||
table = tables.ConsolePortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
|
||||
|
||||
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleport'
|
||||
parent_model = Device
|
||||
@@ -1224,13 +1199,6 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = ConsolePort
|
||||
|
||||
|
||||
class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_consoleport'
|
||||
model_form = forms.ConsolePortCSVForm
|
||||
table = tables.ConsolePortImportTable
|
||||
default_return_url = 'dcim:consoleport_list'
|
||||
|
||||
|
||||
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleport'
|
||||
queryset = ConsolePort.objects.all()
|
||||
@@ -1242,15 +1210,6 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_consoleserverport'
|
||||
queryset = ConsoleServerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
|
||||
filter = filters.ConsoleServerPortFilter
|
||||
filter_form = forms.ConsoleServerPortFilterForm
|
||||
table = tables.ConsoleServerPortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
|
||||
|
||||
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleserverport'
|
||||
parent_model = Device
|
||||
@@ -1272,13 +1231,6 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = ConsoleServerPort
|
||||
|
||||
|
||||
class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_consoleserverport'
|
||||
model_form = forms.ConsoleServerPortCSVForm
|
||||
table = tables.ConsoleServerPortImportTable
|
||||
default_return_url = 'dcim:consoleserverport_list'
|
||||
|
||||
|
||||
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_consoleserverport'
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
@@ -1310,15 +1262,6 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_powerport'
|
||||
queryset = PowerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
|
||||
filter = filters.PowerPortFilter
|
||||
filter_form = forms.PowerPortFilterForm
|
||||
table = tables.PowerPortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
|
||||
|
||||
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_powerport'
|
||||
parent_model = Device
|
||||
@@ -1340,13 +1283,6 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = PowerPort
|
||||
|
||||
|
||||
class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_powerport'
|
||||
model_form = forms.PowerPortCSVForm
|
||||
table = tables.PowerPortImportTable
|
||||
default_return_url = 'dcim:powerport_list'
|
||||
|
||||
|
||||
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerport'
|
||||
queryset = PowerPort.objects.all()
|
||||
@@ -1358,15 +1294,6 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_poweroutlet'
|
||||
queryset = PowerOutlet.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
|
||||
filter = filters.PowerOutletFilter
|
||||
filter_form = forms.PowerOutletFilterForm
|
||||
table = tables.PowerOutletDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
|
||||
|
||||
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_poweroutlet'
|
||||
parent_model = Device
|
||||
@@ -1388,13 +1315,6 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = PowerOutlet
|
||||
|
||||
|
||||
class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_poweroutlet'
|
||||
model_form = forms.PowerOutletCSVForm
|
||||
table = tables.PowerOutletImportTable
|
||||
default_return_url = 'dcim:poweroutlet_list'
|
||||
|
||||
|
||||
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_poweroutlet'
|
||||
queryset = PowerOutlet.objects.all()
|
||||
@@ -1426,15 +1346,6 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_interface'
|
||||
queryset = Interface.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
|
||||
filter = filters.InterfaceFilter
|
||||
filter_form = forms.InterfaceFilterForm
|
||||
table = tables.InterfaceDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
|
||||
|
||||
class InterfaceView(PermissionRequiredMixin, View):
|
||||
permission_required = 'dcim.view_interface'
|
||||
|
||||
@@ -1493,13 +1404,6 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = Interface
|
||||
|
||||
|
||||
class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_interface'
|
||||
model_form = forms.InterfaceCSVForm
|
||||
table = tables.InterfaceImportTable
|
||||
default_return_url = 'dcim:interface_list'
|
||||
|
||||
|
||||
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
queryset = Interface.objects.all()
|
||||
@@ -1531,15 +1435,6 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Front ports
|
||||
#
|
||||
|
||||
class FrontPortListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_frontport'
|
||||
queryset = FrontPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
|
||||
filter = filters.FrontPortFilter
|
||||
filter_form = forms.FrontPortFilterForm
|
||||
table = tables.FrontPortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
|
||||
|
||||
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_frontport'
|
||||
parent_model = Device
|
||||
@@ -1561,13 +1456,6 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = FrontPort
|
||||
|
||||
|
||||
class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_frontport'
|
||||
model_form = forms.FrontPortCSVForm
|
||||
table = tables.FrontPortImportTable
|
||||
default_return_url = 'dcim:frontport_list'
|
||||
|
||||
|
||||
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
@@ -1599,15 +1487,6 @@ class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Rear ports
|
||||
#
|
||||
|
||||
class RearPortListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_rearport'
|
||||
queryset = RearPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
|
||||
filter = filters.RearPortFilter
|
||||
filter_form = forms.RearPortFilterForm
|
||||
table = tables.RearPortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
|
||||
|
||||
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_rearport'
|
||||
parent_model = Device
|
||||
@@ -1629,13 +1508,6 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = RearPort
|
||||
|
||||
|
||||
class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_rearport'
|
||||
model_form = forms.RearPortCSVForm
|
||||
table = tables.RearPortImportTable
|
||||
default_return_url = 'dcim:rearport_list'
|
||||
|
||||
|
||||
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
@@ -1667,17 +1539,6 @@ class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_devicebay'
|
||||
queryset = DeviceBay.objects.prefetch_related(
|
||||
'device', 'device__site', 'installed_device', 'installed_device__site'
|
||||
)
|
||||
filter = filters.DeviceBayFilter
|
||||
filter_form = forms.DeviceBayFilterForm
|
||||
table = tables.DeviceBayDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
|
||||
|
||||
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_devicebay'
|
||||
parent_model = Device
|
||||
@@ -1768,13 +1629,6 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View):
|
||||
})
|
||||
|
||||
|
||||
class DeviceBayBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_devicebay'
|
||||
model_form = forms.DeviceBayCSVForm
|
||||
table = tables.DeviceBayImportTable
|
||||
default_return_url = 'dcim:devicebay_list'
|
||||
|
||||
|
||||
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_devicebay'
|
||||
queryset = DeviceBay.objects.all()
|
||||
@@ -1900,10 +1754,13 @@ class CableTraceView(PermissionRequiredMixin, View):
|
||||
def get(self, request, model, pk):
|
||||
|
||||
obj = get_object_or_404(model, pk=pk)
|
||||
trace = obj.trace(follow_circuits=True)
|
||||
total_length = sum([entry[1]._abs_length for entry in trace if entry[1] and entry[1]._abs_length])
|
||||
|
||||
return render(request, 'dcim/cable_trace.html', {
|
||||
'obj': obj,
|
||||
'trace': obj.trace(follow_circuits=True),
|
||||
'trace': trace,
|
||||
'total_length': total_length,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1 +1,15 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
default_app_config = 'extras.apps.ExtrasConfig'
|
||||
|
||||
# check that django-rq is installed and we can connect to redis
|
||||
if settings.WEBHOOKS_ENABLED:
|
||||
try:
|
||||
import django_rq
|
||||
except ImportError:
|
||||
raise ImproperlyConfigured(
|
||||
"django-rq is not installed! You must install this package per "
|
||||
"the documentation to use the webhook backend."
|
||||
)
|
||||
|
||||
@@ -3,7 +3,10 @@ from django.contrib import admin
|
||||
|
||||
from netbox.admin import admin_site
|
||||
from utilities.forms import LaxURLField
|
||||
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, Webhook
|
||||
from .models import (
|
||||
CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, TopologyMap, Webhook,
|
||||
)
|
||||
from .reports import get_report
|
||||
|
||||
|
||||
def order_content_types(field):
|
||||
@@ -164,3 +167,45 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
'content_type',
|
||||
]
|
||||
form = ExportTemplateForm
|
||||
|
||||
|
||||
#
|
||||
# Reports
|
||||
#
|
||||
|
||||
@admin.register(ReportResult, site=admin_site)
|
||||
class ReportResultAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'report', 'active', 'created', 'user', 'passing',
|
||||
]
|
||||
fields = [
|
||||
'report', 'user', 'passing', 'data',
|
||||
]
|
||||
list_filter = [
|
||||
'failed',
|
||||
]
|
||||
readonly_fields = fields
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def active(self, obj):
|
||||
module, report_name = obj.report.split('.')
|
||||
return True if get_report(module, report_name) else False
|
||||
active.boolean = True
|
||||
|
||||
def passing(self, obj):
|
||||
return not obj.failed
|
||||
passing.boolean = True
|
||||
|
||||
|
||||
#
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
@admin.register(TopologyMap, site=admin_site)
|
||||
class TopologyMapAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'site']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.db import transaction
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from extras.choices import *
|
||||
from extras.constants import *
|
||||
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
@@ -22,7 +22,9 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
def to_internal_value(self, data):
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
|
||||
custom_fields = {field.name: field for field in CustomField.objects.filter(obj_type=content_type)}
|
||||
custom_fields = {
|
||||
field.name: field for field in CustomField.objects.filter(obj_type=content_type)
|
||||
}
|
||||
|
||||
for field_name, value in data.items():
|
||||
|
||||
@@ -37,7 +39,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
if value not in [None, '']:
|
||||
|
||||
# Validate integer
|
||||
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
if cf.type == CF_TYPE_INTEGER:
|
||||
try:
|
||||
int(value)
|
||||
except ValueError:
|
||||
@@ -46,13 +48,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
)
|
||||
|
||||
# Validate boolean
|
||||
if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
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 == CustomFieldTypeChoices.TYPE_DATE:
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
@@ -61,7 +63,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
)
|
||||
|
||||
# Validate selected choice
|
||||
if cf.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
@@ -100,18 +102,18 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
instance.custom_fields = {}
|
||||
for field in fields:
|
||||
value = instance.cf.get(field.name)
|
||||
if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
|
||||
if field.type == CF_TYPE_SELECT and value is not None:
|
||||
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
|
||||
else:
|
||||
instance.custom_fields[field.name] = value
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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)
|
||||
|
||||
# Retrieve the set of CustomFields which apply to this type of object
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
fields = CustomField.objects.filter(obj_type=content_type)
|
||||
if self.instance is not None:
|
||||
|
||||
# Populate CustomFieldValues for each instance from database
|
||||
try:
|
||||
@@ -120,6 +122,26 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
except TypeError:
|
||||
_populate_custom_fields(self.instance, fields)
|
||||
|
||||
else:
|
||||
|
||||
if not hasattr(self, 'initial_data'):
|
||||
self.initial_data = {}
|
||||
|
||||
# Populate default values
|
||||
if fields and 'custom_fields' not in self.initial_data:
|
||||
self.initial_data['custom_fields'] = {}
|
||||
|
||||
# Populate initial data using custom field default values
|
||||
for field in fields:
|
||||
if field.name not in self.initial_data['custom_fields'] and field.default:
|
||||
if field.type == CF_TYPE_SELECT:
|
||||
field_value = field.choices.get(value=field.default).pk
|
||||
elif field.type == CF_TYPE_BOOLEAN:
|
||||
field_value = bool(field.default)
|
||||
else:
|
||||
field_value = field.default
|
||||
self.initial_data['custom_fields'][field.name] = field_value
|
||||
|
||||
def _save_custom_fields(self, instance, custom_fields):
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
for field_name, value in custom_fields.items():
|
||||
|
||||
@@ -8,10 +8,10 @@ from dcim.api.nested_serializers import (
|
||||
NestedRegionSerializer, NestedSiteSerializer,
|
||||
)
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
||||
from extras.choices import *
|
||||
from extras.constants import *
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
Tag
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@@ -28,9 +28,7 @@ from .nested_serializers import *
|
||||
#
|
||||
|
||||
class GraphSerializer(ValidatedModelSerializer):
|
||||
type = ContentTypeField(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = Graph
|
||||
@@ -40,9 +38,7 @@ class GraphSerializer(ValidatedModelSerializer):
|
||||
class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||
embed_url = serializers.SerializerMethodField()
|
||||
embed_link = serializers.SerializerMethodField()
|
||||
type = ContentTypeField(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = Graph
|
||||
@@ -61,8 +57,8 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||
|
||||
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
template_language = ChoiceField(
|
||||
choices=ExportTemplateLanguageChoices,
|
||||
default=ExportTemplateLanguageChoices.LANGUAGE_JINJA2
|
||||
choices=TEMPLATE_LANGUAGE_CHOICES,
|
||||
default=TEMPLATE_LANGUAGE_JINJA2
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -73,6 +69,18 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
class TopologyMapSerializer(ValidatedModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
|
||||
class Meta:
|
||||
model = TopologyMap
|
||||
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
@@ -173,18 +181,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
tags = serializers.SlugRelatedField(
|
||||
queryset=Tag.objects.all(),
|
||||
slug_field='slug',
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
|
||||
'tenant_groups', 'tenants', 'tags', 'data',
|
||||
'tenant_groups', 'tenants', 'data',
|
||||
]
|
||||
|
||||
|
||||
@@ -211,52 +213,6 @@ class ReportDetailSerializer(ReportSerializer):
|
||||
result = ReportResultSerializer()
|
||||
|
||||
|
||||
#
|
||||
# Scripts
|
||||
#
|
||||
|
||||
class ScriptSerializer(serializers.Serializer):
|
||||
id = serializers.SerializerMethodField(read_only=True)
|
||||
name = serializers.SerializerMethodField(read_only=True)
|
||||
description = serializers.SerializerMethodField(read_only=True)
|
||||
vars = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_id(self, instance):
|
||||
return '{}.{}'.format(instance.__module__, instance.__name__)
|
||||
|
||||
def get_name(self, instance):
|
||||
return getattr(instance.Meta, 'name', instance.__name__)
|
||||
|
||||
def get_description(self, instance):
|
||||
return getattr(instance.Meta, 'description', '')
|
||||
|
||||
def get_vars(self, instance):
|
||||
return {
|
||||
k: v.__class__.__name__ for k, v in instance._get_vars().items()
|
||||
}
|
||||
|
||||
|
||||
class ScriptInputSerializer(serializers.Serializer):
|
||||
data = serializers.JSONField()
|
||||
commit = serializers.BooleanField()
|
||||
|
||||
|
||||
class ScriptLogMessageSerializer(serializers.Serializer):
|
||||
status = serializers.SerializerMethodField(read_only=True)
|
||||
message = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_status(self, instance):
|
||||
return LOG_LEVEL_CODES.get(instance[0])
|
||||
|
||||
def get_message(self, instance):
|
||||
return instance[1]
|
||||
|
||||
|
||||
class ScriptOutputSerializer(serializers.Serializer):
|
||||
log = ScriptLogMessageSerializer(many=True, read_only=True)
|
||||
output = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
@@ -266,7 +222,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
read_only=True
|
||||
)
|
||||
action = ChoiceField(
|
||||
choices=ObjectChangeActionChoices,
|
||||
choices=OBJECTCHANGE_ACTION_CHOICES,
|
||||
read_only=True
|
||||
)
|
||||
changed_object_type = ContentTypeField(
|
||||
|
||||
@@ -26,6 +26,9 @@ router.register(r'graphs', views.GraphViewSet)
|
||||
# Export templates
|
||||
router.register(r'export-templates', views.ExportTemplateViewSet)
|
||||
|
||||
# Topology maps
|
||||
router.register(r'topology-maps', views.TopologyMapViewSet)
|
||||
|
||||
# Tags
|
||||
router.register(r'tags', views.TagViewSet)
|
||||
|
||||
@@ -38,9 +41,6 @@ router.register(r'config-contexts', views.ConfigContextViewSet)
|
||||
# Reports
|
||||
router.register(r'reports', views.ReportViewSet, basename='report')
|
||||
|
||||
# Scripts
|
||||
router.register(r'scripts', views.ScriptViewSet, basename='script')
|
||||
|
||||
# Change logging
|
||||
router.register(r'object-changes', views.ObjectChangeViewSet)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ from collections import OrderedDict
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count
|
||||
from django.http import Http404
|
||||
from rest_framework import status
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
@@ -11,10 +11,10 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||
|
||||
from extras import filters
|
||||
from extras.models import (
|
||||
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
|
||||
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
Tag,
|
||||
)
|
||||
from extras.reports import get_report, get_reports
|
||||
from extras.scripts import get_script, get_scripts
|
||||
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||
from . import serializers
|
||||
|
||||
@@ -115,6 +115,34 @@ class ExportTemplateViewSet(ModelViewSet):
|
||||
filterset_class = filters.ExportTemplateFilter
|
||||
|
||||
|
||||
#
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
class TopologyMapViewSet(ModelViewSet):
|
||||
queryset = TopologyMap.objects.prefetch_related('site')
|
||||
serializer_class = serializers.TopologyMapSerializer
|
||||
filterset_class = filters.TopologyMapFilter
|
||||
|
||||
@action(detail=True)
|
||||
def render(self, request, pk):
|
||||
|
||||
tmap = get_object_or_404(TopologyMap, pk=pk)
|
||||
img_format = 'png'
|
||||
|
||||
try:
|
||||
data = tmap.render(img_format=img_format)
|
||||
except Exception as e:
|
||||
return HttpResponse(
|
||||
"There was an error generating the requested graph: %s" % e
|
||||
)
|
||||
|
||||
response = HttpResponse(data, content_type='image/{}'.format(img_format))
|
||||
response['Content-Disposition'] = 'inline; filename="{}.{}"'.format(tmap.slug, img_format)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
@@ -224,56 +252,6 @@ class ReportViewSet(ViewSet):
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
#
|
||||
# Scripts
|
||||
#
|
||||
|
||||
class ScriptViewSet(ViewSet):
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
lookup_value_regex = '[^/]+' # Allow dots
|
||||
|
||||
def _get_script(self, pk):
|
||||
module_name, script_name = pk.split('.')
|
||||
script = get_script(module_name, script_name)
|
||||
if script is None:
|
||||
raise Http404
|
||||
return script
|
||||
|
||||
def list(self, request):
|
||||
|
||||
flat_list = []
|
||||
for script_list in get_scripts().values():
|
||||
flat_list.extend(script_list.values())
|
||||
|
||||
serializer = serializers.ScriptSerializer(flat_list, many=True, context={'request': request})
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
def retrieve(self, request, pk):
|
||||
script = self._get_script(pk)
|
||||
serializer = serializers.ScriptSerializer(script, context={'request': request})
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, pk):
|
||||
"""
|
||||
Run a Script identified as "<module>.<script>".
|
||||
"""
|
||||
script = self._get_script(pk)()
|
||||
input_serializer = serializers.ScriptInputSerializer(data=request.data)
|
||||
|
||||
if input_serializer.is_valid():
|
||||
output = script.run(input_serializer.data['data'])
|
||||
script.output = output
|
||||
output_serializer = serializers.ScriptOutputSerializer(script)
|
||||
|
||||
return Response(output_serializer.data)
|
||||
|
||||
return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
import redis
|
||||
|
||||
|
||||
class ExtrasConfig(AppConfig):
|
||||
@@ -11,18 +10,26 @@ class ExtrasConfig(AppConfig):
|
||||
|
||||
import extras.signals
|
||||
|
||||
# Check that we can connect to the configured Redis database.
|
||||
try:
|
||||
rs = redis.Redis(
|
||||
host=settings.WEBHOOKS_REDIS_HOST,
|
||||
port=settings.WEBHOOKS_REDIS_PORT,
|
||||
db=settings.WEBHOOKS_REDIS_DATABASE,
|
||||
password=settings.WEBHOOKS_REDIS_PASSWORD or None,
|
||||
ssl=settings.WEBHOOKS_REDIS_SSL,
|
||||
)
|
||||
rs.ping()
|
||||
except redis.exceptions.ConnectionError:
|
||||
raise ImproperlyConfigured(
|
||||
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
|
||||
"configuration.py."
|
||||
)
|
||||
# Check that we can connect to the configured Redis database if webhooks are enabled.
|
||||
if settings.WEBHOOKS_ENABLED:
|
||||
try:
|
||||
import redis
|
||||
except ImportError:
|
||||
raise ImproperlyConfigured(
|
||||
"WEBHOOKS_ENABLED is True but the redis Python package is not installed. (Try 'pip install "
|
||||
"redis'.)"
|
||||
)
|
||||
try:
|
||||
rs = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
db=settings.REDIS_DATABASE,
|
||||
password=settings.REDIS_PASSWORD or None,
|
||||
ssl=settings.REDIS_SSL,
|
||||
)
|
||||
rs.ping()
|
||||
except redis.exceptions.ConnectionError:
|
||||
raise ImproperlyConfigured(
|
||||
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
|
||||
"configuration.py."
|
||||
)
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
from utilities.choices import ChoiceSet
|
||||
|
||||
|
||||
#
|
||||
# CustomFields
|
||||
#
|
||||
|
||||
class CustomFieldTypeChoices(ChoiceSet):
|
||||
|
||||
TYPE_TEXT = 'text'
|
||||
TYPE_INTEGER = 'integer'
|
||||
TYPE_BOOLEAN = 'boolean'
|
||||
TYPE_DATE = 'date'
|
||||
TYPE_URL = 'url'
|
||||
TYPE_SELECT = 'select'
|
||||
|
||||
CHOICES = (
|
||||
(TYPE_TEXT, 'Text'),
|
||||
(TYPE_INTEGER, 'Integer'),
|
||||
(TYPE_BOOLEAN, 'Boolean (true/false)'),
|
||||
(TYPE_DATE, 'Date'),
|
||||
(TYPE_URL, 'URL'),
|
||||
(TYPE_SELECT, 'Selection'),
|
||||
)
|
||||
|
||||
LEGACY_MAP = {
|
||||
TYPE_TEXT: 100,
|
||||
TYPE_INTEGER: 200,
|
||||
TYPE_BOOLEAN: 300,
|
||||
TYPE_DATE: 400,
|
||||
TYPE_URL: 500,
|
||||
TYPE_SELECT: 600,
|
||||
}
|
||||
|
||||
|
||||
class CustomFieldFilterLogicChoices(ChoiceSet):
|
||||
|
||||
FILTER_DISABLED = 'disabled'
|
||||
FILTER_LOOSE = 'loose'
|
||||
FILTER_EXACT = 'exact'
|
||||
|
||||
CHOICES = (
|
||||
(FILTER_DISABLED, 'Disabled'),
|
||||
(FILTER_LOOSE, 'Loose'),
|
||||
(FILTER_EXACT, 'Exact'),
|
||||
)
|
||||
|
||||
LEGACY_MAP = {
|
||||
FILTER_DISABLED: 0,
|
||||
FILTER_LOOSE: 1,
|
||||
FILTER_EXACT: 2,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# CustomLinks
|
||||
#
|
||||
|
||||
class CustomLinkButtonClassChoices(ChoiceSet):
|
||||
|
||||
CLASS_DEFAULT = 'default'
|
||||
CLASS_PRIMARY = 'primary'
|
||||
CLASS_SUCCESS = 'success'
|
||||
CLASS_INFO = 'info'
|
||||
CLASS_WARNING = 'warning'
|
||||
CLASS_DANGER = 'danger'
|
||||
CLASS_LINK = 'link'
|
||||
|
||||
CHOICES = (
|
||||
(CLASS_DEFAULT, 'Default'),
|
||||
(CLASS_PRIMARY, 'Primary (blue)'),
|
||||
(CLASS_SUCCESS, 'Success (green)'),
|
||||
(CLASS_INFO, 'Info (aqua)'),
|
||||
(CLASS_WARNING, 'Warning (orange)'),
|
||||
(CLASS_DANGER, 'Danger (red)'),
|
||||
(CLASS_LINK, 'None (link)'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ObjectChanges
|
||||
#
|
||||
|
||||
class ObjectChangeActionChoices(ChoiceSet):
|
||||
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
|
||||
CHOICES = (
|
||||
(ACTION_CREATE, 'Created'),
|
||||
(ACTION_UPDATE, 'Updated'),
|
||||
(ACTION_DELETE, 'Deleted'),
|
||||
)
|
||||
|
||||
LEGACY_MAP = {
|
||||
ACTION_CREATE: 1,
|
||||
ACTION_UPDATE: 2,
|
||||
ACTION_DELETE: 3,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# ExportTemplates
|
||||
#
|
||||
|
||||
class ExportTemplateLanguageChoices(ChoiceSet):
|
||||
|
||||
LANGUAGE_DJANGO = 'django'
|
||||
LANGUAGE_JINJA2 = 'jinja2'
|
||||
|
||||
CHOICES = (
|
||||
(LANGUAGE_DJANGO, 'Django'),
|
||||
(LANGUAGE_JINJA2, 'Jinja2'),
|
||||
)
|
||||
|
||||
LEGACY_MAP = {
|
||||
LANGUAGE_DJANGO: 10,
|
||||
LANGUAGE_JINJA2: 20,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookContentTypeChoices(ChoiceSet):
|
||||
|
||||
CONTENTTYPE_JSON = 'application/json'
|
||||
CONTENTTYPE_FORMDATA = 'application/x-www-form-urlencoded'
|
||||
|
||||
CHOICES = (
|
||||
(CONTENTTYPE_JSON, 'JSON'),
|
||||
(CONTENTTYPE_FORMDATA, 'Form data'),
|
||||
)
|
||||
|
||||
LEGACY_MAP = {
|
||||
CONTENTTYPE_JSON: 1,
|
||||
CONTENTTYPE_FORMDATA: 2,
|
||||
}
|
||||
@@ -19,6 +19,32 @@ CUSTOMFIELD_MODELS = [
|
||||
'virtualization.virtualmachine',
|
||||
]
|
||||
|
||||
# Custom field types
|
||||
CF_TYPE_TEXT = 100
|
||||
CF_TYPE_INTEGER = 200
|
||||
CF_TYPE_BOOLEAN = 300
|
||||
CF_TYPE_DATE = 400
|
||||
CF_TYPE_URL = 500
|
||||
CF_TYPE_SELECT = 600
|
||||
CUSTOMFIELD_TYPE_CHOICES = (
|
||||
(CF_TYPE_TEXT, 'Text'),
|
||||
(CF_TYPE_INTEGER, 'Integer'),
|
||||
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
|
||||
(CF_TYPE_DATE, 'Date'),
|
||||
(CF_TYPE_URL, 'URL'),
|
||||
(CF_TYPE_SELECT, 'Selection'),
|
||||
)
|
||||
|
||||
# Custom field filter logic choices
|
||||
CF_FILTER_DISABLED = 0
|
||||
CF_FILTER_LOOSE = 1
|
||||
CF_FILTER_EXACT = 2
|
||||
CF_FILTER_CHOICES = (
|
||||
(CF_FILTER_DISABLED, 'Disabled'),
|
||||
(CF_FILTER_LOOSE, 'Loose'),
|
||||
(CF_FILTER_EXACT, 'Exact'),
|
||||
)
|
||||
|
||||
# Custom links
|
||||
CUSTOMLINK_MODELS = [
|
||||
'circuits.circuit',
|
||||
@@ -42,6 +68,35 @@ CUSTOMLINK_MODELS = [
|
||||
'virtualization.virtualmachine',
|
||||
]
|
||||
|
||||
BUTTON_CLASS_DEFAULT = 'default'
|
||||
BUTTON_CLASS_PRIMARY = 'primary'
|
||||
BUTTON_CLASS_SUCCESS = 'success'
|
||||
BUTTON_CLASS_INFO = 'info'
|
||||
BUTTON_CLASS_WARNING = 'warning'
|
||||
BUTTON_CLASS_DANGER = 'danger'
|
||||
BUTTON_CLASS_LINK = 'link'
|
||||
BUTTON_CLASS_CHOICES = (
|
||||
(BUTTON_CLASS_DEFAULT, 'Default'),
|
||||
(BUTTON_CLASS_PRIMARY, 'Primary (blue)'),
|
||||
(BUTTON_CLASS_SUCCESS, 'Success (green)'),
|
||||
(BUTTON_CLASS_INFO, 'Info (aqua)'),
|
||||
(BUTTON_CLASS_WARNING, 'Warning (orange)'),
|
||||
(BUTTON_CLASS_DANGER, 'Danger (red)'),
|
||||
(BUTTON_CLASS_LINK, 'None (link)'),
|
||||
)
|
||||
|
||||
# Graph types
|
||||
GRAPH_TYPE_INTERFACE = 100
|
||||
GRAPH_TYPE_DEVICE = 150
|
||||
GRAPH_TYPE_PROVIDER = 200
|
||||
GRAPH_TYPE_SITE = 300
|
||||
GRAPH_TYPE_CHOICES = (
|
||||
(GRAPH_TYPE_INTERFACE, 'Interface'),
|
||||
(GRAPH_TYPE_DEVICE, 'Device'),
|
||||
(GRAPH_TYPE_PROVIDER, 'Provider'),
|
||||
(GRAPH_TYPE_SITE, 'Site'),
|
||||
)
|
||||
|
||||
# Models which support export templates
|
||||
EXPORTTEMPLATE_MODELS = [
|
||||
'circuits.circuit',
|
||||
@@ -73,6 +128,52 @@ EXPORTTEMPLATE_MODELS = [
|
||||
'virtualization.virtualmachine',
|
||||
]
|
||||
|
||||
# ExportTemplate language choices
|
||||
TEMPLATE_LANGUAGE_DJANGO = 10
|
||||
TEMPLATE_LANGUAGE_JINJA2 = 20
|
||||
TEMPLATE_LANGUAGE_CHOICES = (
|
||||
(TEMPLATE_LANGUAGE_DJANGO, 'Django'),
|
||||
(TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'),
|
||||
)
|
||||
|
||||
# Topology map types
|
||||
TOPOLOGYMAP_TYPE_NETWORK = 1
|
||||
TOPOLOGYMAP_TYPE_CONSOLE = 2
|
||||
TOPOLOGYMAP_TYPE_POWER = 3
|
||||
TOPOLOGYMAP_TYPE_CHOICES = (
|
||||
(TOPOLOGYMAP_TYPE_NETWORK, 'Network'),
|
||||
(TOPOLOGYMAP_TYPE_CONSOLE, 'Console'),
|
||||
(TOPOLOGYMAP_TYPE_POWER, 'Power'),
|
||||
)
|
||||
|
||||
# Change log actions
|
||||
OBJECTCHANGE_ACTION_CREATE = 1
|
||||
OBJECTCHANGE_ACTION_UPDATE = 2
|
||||
OBJECTCHANGE_ACTION_DELETE = 3
|
||||
OBJECTCHANGE_ACTION_CHOICES = (
|
||||
(OBJECTCHANGE_ACTION_CREATE, 'Created'),
|
||||
(OBJECTCHANGE_ACTION_UPDATE, 'Updated'),
|
||||
(OBJECTCHANGE_ACTION_DELETE, 'Deleted'),
|
||||
)
|
||||
|
||||
# User action types
|
||||
ACTION_CREATE = 1
|
||||
ACTION_IMPORT = 2
|
||||
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'),
|
||||
)
|
||||
|
||||
# Report logging levels
|
||||
LOG_DEFAULT = 0
|
||||
LOG_SUCCESS = 10
|
||||
@@ -87,6 +188,14 @@ LOG_LEVEL_CODES = {
|
||||
LOG_FAILURE: 'failure',
|
||||
}
|
||||
|
||||
# webhook content types
|
||||
WEBHOOK_CT_JSON = 1
|
||||
WEBHOOK_CT_X_WWW_FORM_ENCODED = 2
|
||||
WEBHOOK_CT_CHOICES = (
|
||||
(WEBHOOK_CT_JSON, 'application/json'),
|
||||
(WEBHOOK_CT_X_WWW_FORM_ENCODED, 'application/x-www-form-urlencoded'),
|
||||
)
|
||||
|
||||
# Models which support registered webhooks
|
||||
WEBHOOK_MODELS = [
|
||||
'circuits.circuit',
|
||||
|
||||
@@ -4,8 +4,22 @@ from django.db.models import Q
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
|
||||
from .constants import *
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag, TopologyMap
|
||||
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextFilter',
|
||||
'CreatedUpdatedFilterSet',
|
||||
'CustomFieldFilter',
|
||||
'CustomFieldFilterSet',
|
||||
'ExportTemplateFilter',
|
||||
'GraphFilter',
|
||||
'LocalConfigContextFilter',
|
||||
'ObjectChangeFilter',
|
||||
'TagFilter',
|
||||
'TopologyMapFilter',
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldFilter(django_filters.Filter):
|
||||
@@ -25,7 +39,7 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
return queryset
|
||||
|
||||
# Selection fields get special treatment (values must be integers)
|
||||
if self.cf_type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
if self.cf_type == CF_TYPE_SELECT:
|
||||
try:
|
||||
# Treat 0 as None
|
||||
if int(value) == 0:
|
||||
@@ -42,8 +56,7 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
return queryset.none()
|
||||
|
||||
# Apply the assigned filter logic (exact or loose)
|
||||
if (self.cf_type == CustomFieldTypeChoices.TYPE_BOOLEAN or
|
||||
self.filter_logic == CustomFieldFilterLogicChoices.FILTER_EXACT):
|
||||
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
|
||||
queryset = queryset.filter(
|
||||
custom_field_values__field__name=self.field_name,
|
||||
custom_field_values__serialized_value=value
|
||||
@@ -66,11 +79,7 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
custom_fields = CustomField.objects.filter(
|
||||
obj_type=obj_type
|
||||
).exclude(
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
||||
)
|
||||
custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
|
||||
for cf in custom_fields:
|
||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
|
||||
|
||||
@@ -108,6 +117,24 @@ class TagFilter(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class TopologyMapFilter(django_filters.FilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TopologyMap
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class ConfigContextFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@@ -179,12 +206,6 @@ class ConfigContextFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tags__slug',
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tag (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
|
||||
@@ -10,10 +10,10 @@ from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||
CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField,
|
||||
SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
|
||||
|
||||
|
||||
@@ -28,18 +28,18 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
field_dict = OrderedDict()
|
||||
custom_fields = CustomField.objects.filter(obj_type=content_type)
|
||||
if filterable_only:
|
||||
custom_fields = custom_fields.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
|
||||
custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
|
||||
|
||||
for cf in custom_fields:
|
||||
field_name = 'cf_{}'.format(str(cf.name))
|
||||
initial = cf.default if not bulk_edit else None
|
||||
|
||||
# Integer
|
||||
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
if cf.type == CF_TYPE_INTEGER:
|
||||
field = forms.IntegerField(required=cf.required, initial=initial)
|
||||
|
||||
# Boolean
|
||||
elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
elif cf.type == CF_TYPE_BOOLEAN:
|
||||
choices = (
|
||||
(None, '---------'),
|
||||
(1, 'True'),
|
||||
@@ -52,15 +52,15 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
else:
|
||||
initial = None
|
||||
field = forms.NullBooleanField(
|
||||
required=cf.required, initial=initial, widget=forms.Select(choices=choices)
|
||||
required=cf.required, initial=initial, widget=StaticSelect2(choices=choices)
|
||||
)
|
||||
|
||||
# Date
|
||||
elif cf.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
|
||||
elif cf.type == CF_TYPE_DATE:
|
||||
field = forms.DateField(required=cf.required, initial=initial, widget=DatePicker())
|
||||
|
||||
# Select
|
||||
elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
elif cf.type == CF_TYPE_SELECT:
|
||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||
if not cf.required or bulk_edit or filterable_only:
|
||||
choices = [(None, '---------')] + choices
|
||||
@@ -71,10 +71,12 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
default_choice = cf.choices.get(value=initial).pk
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
|
||||
field = forms.TypedChoiceField(
|
||||
choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2()
|
||||
)
|
||||
|
||||
# URL
|
||||
elif cf.type == CustomFieldTypeChoices.TYPE_URL:
|
||||
elif cf.type == CF_TYPE_URL:
|
||||
field = LaxURLField(required=cf.required, initial=initial)
|
||||
|
||||
# Text
|
||||
@@ -237,14 +239,6 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
#
|
||||
|
||||
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = forms.ModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/extras/tags/"
|
||||
)
|
||||
)
|
||||
data = JSONField(
|
||||
label=''
|
||||
)
|
||||
@@ -253,7 +247,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
|
||||
'tenants', 'tags', 'data',
|
||||
'tenants', 'data',
|
||||
]
|
||||
widgets = {
|
||||
'regions': APISelectMultiple(
|
||||
@@ -273,7 +267,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
),
|
||||
'tenants': APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -354,14 +348,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tag = FilterChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/extras/tags/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -404,19 +390,15 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
time_after = forms.DateTimeField(
|
||||
label='After',
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
||||
)
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
time_before = forms.DateTimeField(
|
||||
label='Before',
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
||||
)
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
action = forms.ChoiceField(
|
||||
choices=add_blank_choice(ObjectChangeActionChoices),
|
||||
choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
|
||||
required=False
|
||||
)
|
||||
user = forms.ModelChoiceField(
|
||||
|
||||
@@ -9,9 +9,8 @@ from django.db.models.signals import pre_delete, post_save
|
||||
from django.utils import timezone
|
||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||
|
||||
from extras.utils import is_taggable
|
||||
from utilities.querysets import DummyQuerySet
|
||||
from .choices import ObjectChangeActionChoices
|
||||
from .constants import *
|
||||
from .models import ObjectChange
|
||||
from .signals import purge_changelog
|
||||
from .webhooks import enqueue_webhooks
|
||||
@@ -24,7 +23,7 @@ def handle_changed_object(sender, instance, **kwargs):
|
||||
Fires when an object is created or updated.
|
||||
"""
|
||||
# Queue the object for processing once the request completes
|
||||
action = ObjectChangeActionChoices.ACTION_CREATE if kwargs['created'] else ObjectChangeActionChoices.ACTION_UPDATE
|
||||
action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
|
||||
_thread_locals.changed_objects.append(
|
||||
(instance, action)
|
||||
)
|
||||
@@ -42,12 +41,12 @@ def handle_deleted_object(sender, instance, **kwargs):
|
||||
copy = deepcopy(instance)
|
||||
|
||||
# Preserve tags
|
||||
if is_taggable(instance):
|
||||
if hasattr(instance, 'tags'):
|
||||
copy.tags = DummyQuerySet(instance.tags.all())
|
||||
|
||||
# Queue the copy of the object for processing once the request completes
|
||||
_thread_locals.changed_objects.append(
|
||||
(copy, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
(copy, OBJECTCHANGE_ACTION_DELETE)
|
||||
)
|
||||
|
||||
|
||||
@@ -102,7 +101,7 @@ class ObjectChangeMiddleware(object):
|
||||
for instance, action in _thread_locals.changed_objects:
|
||||
|
||||
# Refresh cached custom field values
|
||||
if action in [ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]:
|
||||
if action in [OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_UPDATE]:
|
||||
if hasattr(instance, 'cache_custom_fields'):
|
||||
instance.cache_custom_fields()
|
||||
|
||||
@@ -117,11 +116,11 @@ class ObjectChangeMiddleware(object):
|
||||
enqueue_webhooks(instance, request.user, request.id, action)
|
||||
|
||||
# Increment metric counters
|
||||
if action == ObjectChangeActionChoices.ACTION_CREATE:
|
||||
if action == OBJECTCHANGE_ACTION_CREATE:
|
||||
model_inserts.labels(instance._meta.model_name).inc()
|
||||
elif action == ObjectChangeActionChoices.ACTION_UPDATE:
|
||||
elif action == OBJECTCHANGE_ACTION_UPDATE:
|
||||
model_updates.labels(instance._meta.model_name).inc()
|
||||
elif action == ObjectChangeActionChoices.ACTION_DELETE:
|
||||
elif action == OBJECTCHANGE_ACTION_DELETE:
|
||||
model_deletes.labels(instance._meta.model_name).inc()
|
||||
|
||||
# Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in
|
||||
|
||||
@@ -8,6 +8,8 @@ import django.db.models.deletion
|
||||
import extras.models
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
|
||||
|
||||
|
||||
def verify_postgresql_version(apps, schema_editor):
|
||||
"""
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
# Generated by Django 1.11.9 on 2018-02-21 19:48
|
||||
from django.db import migrations, models
|
||||
|
||||
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
|
||||
|
||||
|
||||
def is_filterable_to_filter_logic(apps, schema_editor):
|
||||
CustomField = apps.get_model('extras', 'CustomField')
|
||||
CustomField.objects.filter(is_filterable=False).update(filter_logic=0)
|
||||
CustomField.objects.filter(is_filterable=True).update(filter_logic=1)
|
||||
CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED)
|
||||
CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE)
|
||||
# Select fields match on primary key only
|
||||
CustomField.objects.filter(is_filterable=True, type=600).update(filter_logic=2)
|
||||
CustomField.objects.filter(is_filterable=True, type=CF_TYPE_SELECT).update(filter_logic=CF_FILTER_EXACT)
|
||||
|
||||
|
||||
def filter_logic_to_is_filterable(apps, schema_editor):
|
||||
CustomField = apps.get_model('extras', 'CustomField')
|
||||
CustomField.objects.filter(filter_logic=0).update(is_filterable=False)
|
||||
CustomField.objects.exclude(filter_logic=0).update(is_filterable=True)
|
||||
CustomField.objects.filter(filter_logic=CF_FILTER_DISABLED).update(is_filterable=False)
|
||||
CustomField.objects.exclude(filter_logic=CF_FILTER_DISABLED).update(is_filterable=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Generated by Django 2.2 on 2019-08-09 01:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0027_webhook_additional_headers'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='TopologyMap',
|
||||
),
|
||||
]
|
||||
@@ -1,69 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
CUSTOMFIELD_TYPE_CHOICES = (
|
||||
(100, 'text'),
|
||||
(200, 'integer'),
|
||||
(300, 'boolean'),
|
||||
(400, 'date'),
|
||||
(500, 'url'),
|
||||
(600, 'select')
|
||||
)
|
||||
|
||||
CUSTOMFIELD_FILTER_LOGIC_CHOICES = (
|
||||
(0, 'disabled'),
|
||||
(1, 'integer'),
|
||||
(2, 'exact'),
|
||||
)
|
||||
|
||||
|
||||
def customfield_type_to_slug(apps, schema_editor):
|
||||
CustomField = apps.get_model('extras', 'CustomField')
|
||||
for id, slug in CUSTOMFIELD_TYPE_CHOICES:
|
||||
CustomField.objects.filter(type=str(id)).update(type=slug)
|
||||
|
||||
|
||||
def customfield_filter_logic_to_slug(apps, schema_editor):
|
||||
CustomField = apps.get_model('extras', 'CustomField')
|
||||
for id, slug in CUSTOMFIELD_FILTER_LOGIC_CHOICES:
|
||||
CustomField.objects.filter(filter_logic=str(id)).update(filter_logic=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('extras', '0028_remove_topology_maps'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# CustomField.type
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='type',
|
||||
field=models.CharField(default='text', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=customfield_type_to_slug
|
||||
),
|
||||
|
||||
# Update CustomFieldChoice.field.limit_choices_to
|
||||
migrations.AlterField(
|
||||
model_name='customfieldchoice',
|
||||
name='field',
|
||||
field=models.ForeignKey(limit_choices_to={'type': 'select'}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'),
|
||||
),
|
||||
|
||||
# CustomField.filter_logic
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='filter_logic',
|
||||
field=models.CharField(default='loose', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=customfield_filter_logic_to_slug
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,36 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
OBJECTCHANGE_ACTION_CHOICES = (
|
||||
(1, 'create'),
|
||||
(2, 'update'),
|
||||
(3, 'delete'),
|
||||
)
|
||||
|
||||
|
||||
def objectchange_action_to_slug(apps, schema_editor):
|
||||
ObjectChange = apps.get_model('extras', 'ObjectChange')
|
||||
for id, slug in OBJECTCHANGE_ACTION_CHOICES:
|
||||
ObjectChange.objects.filter(action=str(id)).update(action=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('extras', '0029_3569_customfield_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# ObjectChange.action
|
||||
migrations.AlterField(
|
||||
model_name='objectchange',
|
||||
name='action',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=objectchange_action_to_slug
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,35 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
EXPORTTEMPLATE_LANGUAGE_CHOICES = (
|
||||
(10, 'django'),
|
||||
(20, 'jinja2'),
|
||||
)
|
||||
|
||||
|
||||
def exporttemplate_language_to_slug(apps, schema_editor):
|
||||
ExportTemplate = apps.get_model('extras', 'ExportTemplate')
|
||||
for id, slug in EXPORTTEMPLATE_LANGUAGE_CHOICES:
|
||||
ExportTemplate.objects.filter(template_language=str(id)).update(template_language=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('extras', '0030_3569_objectchange_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# ExportTemplate.template_language
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='template_language',
|
||||
field=models.CharField(default='jinja2', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=exporttemplate_language_to_slug
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,35 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
WEBHOOK_CONTENTTYPE_CHOICES = (
|
||||
(1, 'application/json'),
|
||||
(2, 'application/x-www-form-urlencoded'),
|
||||
)
|
||||
|
||||
|
||||
def webhook_contenttype_to_slug(apps, schema_editor):
|
||||
Webhook = apps.get_model('extras', 'Webhook')
|
||||
for id, slug in WEBHOOK_CONTENTTYPE_CHOICES:
|
||||
Webhook.objects.filter(http_content_type=str(id)).update(http_content_type=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('extras', '0031_3569_exporttemplate_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# Webhook.http_content_type
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='http_content_type',
|
||||
field=models.CharField(default='application/json', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=webhook_contenttype_to_slug
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,46 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
GRAPH_TYPE_CHOICES = (
|
||||
(100, 'dcim', 'interface'),
|
||||
(150, 'dcim', 'device'),
|
||||
(200, 'circuits', 'provider'),
|
||||
(300, 'dcim', 'site'),
|
||||
)
|
||||
|
||||
|
||||
def graph_type_to_fk(apps, schema_editor):
|
||||
Graph = apps.get_model('extras', 'Graph')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
# On a new installation (and during tests) content types might not yet exist. So, we only perform the bulk
|
||||
# updates if a Graph has been created, which implies that we're working with a populated database.
|
||||
if Graph.objects.exists():
|
||||
for id, app_label, model in GRAPH_TYPE_CHOICES:
|
||||
content_type = ContentType.objects.get(app_label=app_label, model=model)
|
||||
Graph.objects.filter(type=id).update(type=content_type.pk)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0032_3569_webhook_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# We have to swap the legacy IDs to ContentType PKs *before* we alter the field, to avoid triggering an
|
||||
# IntegrityError on the ForeignKey.
|
||||
migrations.RunPython(
|
||||
code=graph_type_to_fk
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='graph',
|
||||
name='type',
|
||||
field=models.ForeignKey(
|
||||
limit_choices_to={'model__in': ['device', 'interface', 'provider', 'site']},
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='contenttypes.ContentType'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.6 on 2019-12-11 09:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0033_graph_type_to_fk'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='configcontext',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, related_name='_configcontext_tags_+', to='extras.Tag'),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +1,22 @@
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
|
||||
import graphviz
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context
|
||||
from django.urls import reverse
|
||||
from jinja2 import Environment
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||
from utilities.fields import ColorField
|
||||
from utilities.utils import deepmerge, model_names_to_filter_dict
|
||||
from .choices import *
|
||||
from utilities.utils import deepmerge, foreground_color, model_names_to_filter_dict, render_jinja2
|
||||
from .constants import *
|
||||
from .querysets import ConfigContextQuerySet
|
||||
|
||||
@@ -63,10 +64,9 @@ class Webhook(models.Model):
|
||||
verbose_name='URL',
|
||||
help_text="A POST will be sent to this URL when the webhook is called."
|
||||
)
|
||||
http_content_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=WebhookContentTypeChoices,
|
||||
default=WebhookContentTypeChoices.CONTENTTYPE_JSON,
|
||||
http_content_type = models.PositiveSmallIntegerField(
|
||||
choices=WEBHOOK_CT_CHOICES,
|
||||
default=WEBHOOK_CT_JSON,
|
||||
verbose_name='HTTP content type'
|
||||
)
|
||||
additional_headers = JSONField(
|
||||
@@ -184,10 +184,9 @@ class CustomField(models.Model):
|
||||
limit_choices_to=get_custom_field_models,
|
||||
help_text='The object(s) to which this field applies.'
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=CustomFieldTypeChoices,
|
||||
default=CustomFieldTypeChoices.TYPE_TEXT
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=CUSTOMFIELD_TYPE_CHOICES,
|
||||
default=CF_TYPE_TEXT
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
@@ -208,10 +207,9 @@ class CustomField(models.Model):
|
||||
help_text='If true, this field is required when creating new objects '
|
||||
'or editing an existing object.'
|
||||
)
|
||||
filter_logic = models.CharField(
|
||||
max_length=50,
|
||||
choices=CustomFieldFilterLogicChoices,
|
||||
default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
|
||||
filter_logic = models.PositiveSmallIntegerField(
|
||||
choices=CF_FILTER_CHOICES,
|
||||
default=CF_FILTER_LOOSE,
|
||||
help_text='Loose matches any instance of a given string; exact '
|
||||
'matches the entire field.'
|
||||
)
|
||||
@@ -237,15 +235,15 @@ class CustomField(models.Model):
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
if self.type == CF_TYPE_BOOLEAN:
|
||||
return str(int(bool(value)))
|
||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
if self.type == CF_TYPE_DATE:
|
||||
# Could be date/datetime object or string
|
||||
try:
|
||||
return value.strftime('%Y-%m-%d')
|
||||
except AttributeError:
|
||||
return value
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
if self.type == CF_TYPE_SELECT:
|
||||
# Could be ModelChoiceField or TypedChoiceField
|
||||
return str(value.id) if hasattr(value, 'id') else str(value)
|
||||
return value
|
||||
@@ -256,14 +254,14 @@ class CustomField(models.Model):
|
||||
"""
|
||||
if serialized_value == '':
|
||||
return None
|
||||
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
if self.type == CF_TYPE_INTEGER:
|
||||
return int(serialized_value)
|
||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
if self.type == CF_TYPE_BOOLEAN:
|
||||
return bool(int(serialized_value))
|
||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
if self.type == CF_TYPE_DATE:
|
||||
# Read date as YYYY-MM-DD
|
||||
return date(*[int(n) for n in serialized_value.split('-')])
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
if self.type == CF_TYPE_SELECT:
|
||||
return self.choices.get(pk=int(serialized_value))
|
||||
return serialized_value
|
||||
|
||||
@@ -316,7 +314,7 @@ class CustomFieldChoice(models.Model):
|
||||
to='extras.CustomField',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='choices',
|
||||
limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT}
|
||||
limit_choices_to={'type': CF_TYPE_SELECT}
|
||||
)
|
||||
value = models.CharField(
|
||||
max_length=100
|
||||
@@ -334,17 +332,14 @@ class CustomFieldChoice(models.Model):
|
||||
return self.value
|
||||
|
||||
def clean(self):
|
||||
if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
|
||||
if self.field.type != CF_TYPE_SELECT:
|
||||
raise ValidationError("Custom field choices can only be assigned to selection fields.")
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
|
||||
pk = self.pk
|
||||
super().delete(using, keep_parents)
|
||||
CustomFieldValue.objects.filter(
|
||||
field__type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
serialized_value=str(pk)
|
||||
).delete()
|
||||
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
|
||||
|
||||
|
||||
#
|
||||
@@ -388,8 +383,8 @@ class CustomLink(models.Model):
|
||||
)
|
||||
button_class = models.CharField(
|
||||
max_length=30,
|
||||
choices=CustomLinkButtonClassChoices,
|
||||
default=CustomLinkButtonClassChoices.CLASS_DEFAULT,
|
||||
choices=BUTTON_CLASS_CHOICES,
|
||||
default=BUTTON_CLASS_DEFAULT,
|
||||
help_text="The class of the first link in a group will be used for the dropdown button"
|
||||
)
|
||||
new_window = models.BooleanField(
|
||||
@@ -408,12 +403,8 @@ class CustomLink(models.Model):
|
||||
#
|
||||
|
||||
class Graph(models.Model):
|
||||
type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
limit_choices_to={
|
||||
'model__in': ['device', 'interface', 'provider', 'site']
|
||||
}
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=GRAPH_TYPE_CHOICES
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=1000
|
||||
@@ -469,10 +460,9 @@ class ExportTemplate(models.Model):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
template_language = models.CharField(
|
||||
max_length=50,
|
||||
choices=ExportTemplateLanguageChoices,
|
||||
default=ExportTemplateLanguageChoices.LANGUAGE_JINJA2
|
||||
template_language = models.PositiveSmallIntegerField(
|
||||
choices=TEMPLATE_LANGUAGE_CHOICES,
|
||||
default=TEMPLATE_LANGUAGE_JINJA2
|
||||
)
|
||||
template_code = models.TextField(
|
||||
help_text='The list of objects being exported is passed as a context variable named <code>queryset</code>.'
|
||||
@@ -506,13 +496,12 @@ class ExportTemplate(models.Model):
|
||||
'queryset': queryset
|
||||
}
|
||||
|
||||
if self.template_language == ExportTemplateLanguageChoices.LANGUAGE_DJANGO:
|
||||
if self.template_language == TEMPLATE_LANGUAGE_DJANGO:
|
||||
template = Template(self.template_code)
|
||||
output = template.render(Context(context))
|
||||
|
||||
elif self.template_language == ExportTemplateLanguageChoices.LANGUAGE_JINJA2:
|
||||
template = Environment().from_string(source=self.template_code)
|
||||
output = template.render(**context)
|
||||
elif self.template_language == TEMPLATE_LANGUAGE_JINJA2:
|
||||
output = render_jinja2(self.template_code, context)
|
||||
|
||||
else:
|
||||
return None
|
||||
@@ -540,6 +529,154 @@ class ExportTemplate(models.Model):
|
||||
return response
|
||||
|
||||
|
||||
#
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
class TopologyMap(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
unique=True
|
||||
)
|
||||
slug = models.SlugField(
|
||||
unique=True
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=TOPOLOGYMAP_TYPE_CHOICES,
|
||||
default=TOPOLOGYMAP_TYPE_NETWORK
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='topology_maps',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
device_patterns = models.TextField(
|
||||
help_text='Identify devices to include in the diagram using regular '
|
||||
'expressions, one per line. Each line will result in a new '
|
||||
'tier of the drawing. Separate multiple regexes within a '
|
||||
'line using semicolons. Devices will be rendered in the '
|
||||
'order they are defined.'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def device_sets(self):
|
||||
if not self.device_patterns:
|
||||
return None
|
||||
return [line.strip() for line in self.device_patterns.split('\n')]
|
||||
|
||||
def render(self, img_format='png'):
|
||||
|
||||
from dcim.models import Device
|
||||
|
||||
# Construct the graph
|
||||
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
|
||||
G = graphviz.Graph
|
||||
else:
|
||||
G = graphviz.Digraph
|
||||
self.graph = G()
|
||||
self.graph.graph_attr['ranksep'] = '1'
|
||||
seen = set()
|
||||
for i, device_set in enumerate(self.device_sets):
|
||||
|
||||
subgraph = G(name='sg{}'.format(i))
|
||||
subgraph.graph_attr['rank'] = 'same'
|
||||
subgraph.graph_attr['directed'] = 'true'
|
||||
|
||||
# Add a pseudonode for each device_set to enforce hierarchical layout
|
||||
subgraph.node('set{}'.format(i), label='', shape='none', width='0')
|
||||
if i:
|
||||
self.graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
|
||||
|
||||
# Add each device to the graph
|
||||
devices = []
|
||||
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
|
||||
devices += Device.objects.filter(name__regex=query).prefetch_related('device_role')
|
||||
# Remove duplicate devices
|
||||
devices = [d for d in devices if d.id not in seen]
|
||||
seen.update([d.id for d in devices])
|
||||
for d in devices:
|
||||
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):
|
||||
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
|
||||
|
||||
self.graph.subgraph(subgraph)
|
||||
|
||||
# Compile list of all devices
|
||||
device_superset = Q()
|
||||
for device_set in self.device_sets:
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
device_superset = device_superset | Q(name__regex=query)
|
||||
devices = Device.objects.filter(*(device_superset,))
|
||||
|
||||
# Draw edges depending on graph type
|
||||
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
|
||||
self.add_network_connections(devices)
|
||||
elif self.type == TOPOLOGYMAP_TYPE_CONSOLE:
|
||||
self.add_console_connections(devices)
|
||||
elif self.type == TOPOLOGYMAP_TYPE_POWER:
|
||||
self.add_power_connections(devices)
|
||||
|
||||
return self.graph.pipe(format=img_format)
|
||||
|
||||
def add_network_connections(self, devices):
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import Interface
|
||||
|
||||
# Add all interface connections to the graph
|
||||
connected_interfaces = Interface.objects.prefetch_related(
|
||||
'_connected_interface__device'
|
||||
).filter(
|
||||
Q(device__in=devices) | Q(_connected_interface__device__in=devices),
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
)
|
||||
for interface in connected_interfaces:
|
||||
style = 'solid' if interface.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(interface.device.name, interface.connected_endpoint.device.name, style=style)
|
||||
|
||||
# Add all circuits to the graph
|
||||
for termination in CircuitTermination.objects.filter(term_side='A', connected_endpoint__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):
|
||||
self.graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
|
||||
|
||||
def add_console_connections(self, devices):
|
||||
|
||||
from dcim.models import ConsolePort
|
||||
|
||||
# Add all console connections to the graph
|
||||
for cp in ConsolePort.objects.filter(device__in=devices, connected_endpoint__device__in=devices):
|
||||
style = 'solid' if cp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(cp.connected_endpoint.device.name, cp.device.name, style=style)
|
||||
|
||||
def add_power_connections(self, devices):
|
||||
|
||||
from dcim.models import PowerPort
|
||||
|
||||
# Add all power connections to the graph
|
||||
for pp in PowerPort.objects.filter(device__in=devices, _connected_poweroutlet__device__in=devices):
|
||||
style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(pp.connected_endpoint.device.name, pp.device.name, style=style)
|
||||
|
||||
|
||||
#
|
||||
# Image attachments
|
||||
#
|
||||
@@ -611,20 +748,11 @@ class ImageAttachment(models.Model):
|
||||
@property
|
||||
def size(self):
|
||||
"""
|
||||
Wrapper around `image.size` to suppress an OSError in case the file is inaccessible. Also opportunistically
|
||||
catch other exceptions that we know other storage back-ends to throw.
|
||||
Wrapper around `image.size` to suppress an OSError in case the file is inaccessible.
|
||||
"""
|
||||
expected_exceptions = [OSError]
|
||||
|
||||
try:
|
||||
from botocore.exceptions import ClientError
|
||||
expected_exceptions.append(ClientError)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return self.image.size
|
||||
except tuple(expected_exceptions):
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -682,11 +810,6 @@ class ConfigContext(models.Model):
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tags = models.ManyToManyField(
|
||||
to='extras.Tag',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
data = JSONField()
|
||||
|
||||
objects = ConfigContextQuerySet.as_manager()
|
||||
@@ -792,6 +915,13 @@ class ReportResult(models.Model):
|
||||
class Meta:
|
||||
ordering = ['report']
|
||||
|
||||
def __str__(self):
|
||||
return "{} {} at {}".format(
|
||||
self.report,
|
||||
"passed" if not self.failed else "failed",
|
||||
self.created
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
@@ -822,9 +952,8 @@ class ObjectChange(models.Model):
|
||||
request_id = models.UUIDField(
|
||||
editable=False
|
||||
)
|
||||
action = models.CharField(
|
||||
max_length=50,
|
||||
choices=ObjectChangeActionChoices
|
||||
action = models.PositiveSmallIntegerField(
|
||||
choices=OBJECTCHANGE_ACTION_CHOICES
|
||||
)
|
||||
changed_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user