mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-22 18:08:06 +01:00
Compare commits
344 Commits
v2.5-beta2
...
v2.5.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d112b6027a | ||
|
|
88933a3120 | ||
|
|
e05871b467 | ||
|
|
7e70bfaacc | ||
|
|
f4b85751bb | ||
|
|
ea11e70e3f | ||
|
|
bd8b239e15 | ||
|
|
7a8fc8dfd5 | ||
|
|
4334f1bc65 | ||
|
|
1995091169 | ||
|
|
ef089d3722 | ||
|
|
1d904b1722 | ||
|
|
ab02f26a0e | ||
|
|
f35b4bf768 | ||
|
|
f2382dd255 | ||
|
|
8df169b170 | ||
|
|
88aeaaffb0 | ||
|
|
e9546b810c | ||
|
|
c208d8fc2e | ||
|
|
7294f43fa3 | ||
|
|
0572c66c17 | ||
|
|
6406e213bd | ||
|
|
cfb56f7cfe | ||
|
|
bd65e782bb | ||
|
|
0c142f2078 | ||
|
|
7295c3554e | ||
|
|
78725b8483 | ||
|
|
e97ad3f066 | ||
|
|
3a62e9a322 | ||
|
|
4d18d9661b | ||
|
|
b381bdec27 | ||
|
|
6f5c35c278 | ||
|
|
3e6033e9ff | ||
|
|
32f63a18ff | ||
|
|
00aaf500de | ||
|
|
f2471aedb2 | ||
|
|
beff774295 | ||
|
|
8dfef83f1a | ||
|
|
d707844c30 | ||
|
|
3ffea43253 | ||
|
|
77c387a559 | ||
|
|
ac1e4b8e8f | ||
|
|
36101f50c3 | ||
|
|
08dad7f7af | ||
|
|
298152bc50 | ||
|
|
993e94c00c | ||
|
|
4c3a7b84c1 | ||
|
|
8961a54a03 | ||
|
|
2184a9402f | ||
|
|
b9f1d14d4e | ||
|
|
fba154386e | ||
|
|
c8366eff8f | ||
|
|
161d9ed512 | ||
|
|
fadc9521f0 | ||
|
|
40cb47868f | ||
|
|
9d055ff4fd | ||
|
|
9d69f14faa | ||
|
|
8e3ea6c878 | ||
|
|
971f3cd63c | ||
|
|
a9d7a7e306 | ||
|
|
8797298a71 | ||
|
|
3bc182e453 | ||
|
|
7a0ab3aa15 | ||
|
|
fdbef8ee71 | ||
|
|
d95b127378 | ||
|
|
008138cd02 | ||
|
|
2a1630e068 | ||
|
|
31611203eb | ||
|
|
1ee6d16d78 | ||
|
|
0e8c3a8efe | ||
|
|
d084d19675 | ||
|
|
77954a3796 | ||
|
|
8152dc4b04 | ||
|
|
109b233e14 | ||
|
|
cc3b26998b | ||
|
|
95dea1faaa | ||
|
|
57fecdbf17 | ||
|
|
3b4bcc881f | ||
|
|
dfa4dfa4a4 | ||
|
|
8d86e97247 | ||
|
|
5da9d6b46b | ||
|
|
100809f11a | ||
|
|
5256077a3c | ||
|
|
375e66047d | ||
|
|
42d1d6e1b0 | ||
|
|
ca51fab4d8 | ||
|
|
73c983516d | ||
|
|
f733d5a4da | ||
|
|
3d2948daf3 | ||
|
|
69a5d3644a | ||
|
|
2f1018c742 | ||
|
|
d5fc37282f | ||
|
|
525ed359cd | ||
|
|
fe00db62d6 | ||
|
|
bcfa760cf9 | ||
|
|
613e8f05c2 | ||
|
|
59f8f0c7ea | ||
|
|
ae0c8deec2 | ||
|
|
b508415983 | ||
|
|
a98d014763 | ||
|
|
51e5e49d3b | ||
|
|
0eced489da | ||
|
|
5138d12942 | ||
|
|
fe30276db2 | ||
|
|
e51e8b5c8a | ||
|
|
170900e80f | ||
|
|
6726403de9 | ||
|
|
f6d18d243e | ||
|
|
8bd9b258a8 | ||
|
|
0256448dd8 | ||
|
|
dc70fdbe03 | ||
|
|
ce1a2875bc | ||
|
|
b4c9ec27e0 | ||
|
|
8977ded7b6 | ||
|
|
afb4c636fe | ||
|
|
ff40a13f29 | ||
|
|
9991985170 | ||
|
|
61a48320af | ||
|
|
d53249060d | ||
|
|
22eebbbc71 | ||
|
|
4227c6b806 | ||
|
|
14695037da | ||
|
|
0d717cdc82 | ||
|
|
ae8c5ae7b8 | ||
|
|
fb0ed3db2f | ||
|
|
c69fad7429 | ||
|
|
92a2f529e3 | ||
|
|
bd74e2f30b | ||
|
|
a950c95416 | ||
|
|
dc9e9fd08f | ||
|
|
1abbaf99dc | ||
|
|
af6bb53a01 | ||
|
|
2d7e5a57e7 | ||
|
|
b6737aff59 | ||
|
|
c5f2cbf9fa | ||
|
|
d6d8b078b9 | ||
|
|
1d7a7e2d1d | ||
|
|
4a290f3834 | ||
|
|
9e492cbb4d | ||
|
|
e17d79e10f | ||
|
|
28a2981a4f | ||
|
|
d356e288a2 | ||
|
|
dd5f37391f | ||
|
|
17d6584ef4 | ||
|
|
ad4fb3ce8b | ||
|
|
5f1f8ee73b | ||
|
|
60224be272 | ||
|
|
6dcd48fef1 | ||
|
|
951e7a68e9 | ||
|
|
86bafbb760 | ||
|
|
a6564c49e2 | ||
|
|
c89735cd4e | ||
|
|
f3216abebf | ||
|
|
5676bd15dd | ||
|
|
bf8d57c7d1 | ||
|
|
0d415d94a5 | ||
|
|
73a1d6a7ba | ||
|
|
72d5c6fd1b | ||
|
|
6d5d9c8af3 | ||
|
|
c27cea981c | ||
|
|
f7f6704fc1 | ||
|
|
fca97f9768 | ||
|
|
7a5a73ce34 | ||
|
|
170e01b549 | ||
|
|
99dc46a89e | ||
|
|
848aa0b098 | ||
|
|
81a0889568 | ||
|
|
0a820d9c98 | ||
|
|
209a9f0ffc | ||
|
|
3101a86381 | ||
|
|
27ca0d0930 | ||
|
|
6ca045e1a9 | ||
|
|
0c86693dc4 | ||
|
|
5285b6926f | ||
|
|
f3cfc17a52 | ||
|
|
c58166137c | ||
|
|
28a02e9943 | ||
|
|
c6d9206dd1 | ||
|
|
d144d3a584 | ||
|
|
8cf8710130 | ||
|
|
3705e37678 | ||
|
|
ebe5193348 | ||
|
|
a3097d254e | ||
|
|
38276d9539 | ||
|
|
91a2168952 | ||
|
|
4a10b4ece0 | ||
|
|
853b1fad15 | ||
|
|
7acbeb55bc | ||
|
|
8498e0088b | ||
|
|
aae10f7d71 | ||
|
|
6b19a2b101 | ||
|
|
b44a76e6bd | ||
|
|
7f71fc1d42 | ||
|
|
ba9fe408bc | ||
|
|
40cb576e11 | ||
|
|
2f1db2fdf3 | ||
|
|
f4a22e5af3 | ||
|
|
aca57ec281 | ||
|
|
68cb8b6895 | ||
|
|
82e8c0152e | ||
|
|
f499f2dd66 | ||
|
|
d4a9318826 | ||
|
|
27a893a9a1 | ||
|
|
9f1fcca5ea | ||
|
|
bb564363d5 | ||
|
|
dd2a6a41da | ||
|
|
a6c8c615eb | ||
|
|
0d3b1bfca4 | ||
|
|
edd763b1aa | ||
|
|
2418fed65b | ||
|
|
785cdcefd6 | ||
|
|
3480832bf5 | ||
|
|
ee038bd77b | ||
|
|
6460c95e00 | ||
|
|
b0a6781623 | ||
|
|
8364e56e86 | ||
|
|
b8a4316297 | ||
|
|
24d1707693 | ||
|
|
b4f79f1667 | ||
|
|
064dd9bef2 | ||
|
|
b697c30941 | ||
|
|
93c95fdfa8 | ||
|
|
8863a3126d | ||
|
|
acbe5f6418 | ||
|
|
4e6652d811 | ||
|
|
7d4fa69595 | ||
|
|
baeb7937fc | ||
|
|
2bd9f8a11f | ||
|
|
44a2919a29 | ||
|
|
77fbc42f75 | ||
|
|
65edffea63 | ||
|
|
bf0083552d | ||
|
|
869194354c | ||
|
|
aa8c836b94 | ||
|
|
9689ba2c4f | ||
|
|
703be259fd | ||
|
|
45a1dfbd8a | ||
|
|
360303f86c | ||
|
|
64d37cd450 | ||
|
|
71dee2758b | ||
|
|
870edbb44a | ||
|
|
2a07e8f3f0 | ||
|
|
686a65880e | ||
|
|
ab4cb46d94 | ||
|
|
d3d6c83fbb | ||
|
|
4e3567659a | ||
|
|
f0874f4be0 | ||
|
|
dffa2d3556 | ||
|
|
7bbf33ee39 | ||
|
|
90e7080b63 | ||
|
|
e6ee26cf0e | ||
|
|
0dcab07519 | ||
|
|
a3ade01224 | ||
|
|
232e6f5076 | ||
|
|
d1cd366dc9 | ||
|
|
a1a9396287 | ||
|
|
ca0248c3a2 | ||
|
|
a43fc0d3d3 | ||
|
|
08b4b24296 | ||
|
|
5acd429c55 | ||
|
|
6c2a9107dd | ||
|
|
879d879e56 | ||
|
|
c6d048ca51 | ||
|
|
112aaea51f | ||
|
|
c3cdf8e97e | ||
|
|
d2744700c6 | ||
|
|
5d07a5a670 | ||
|
|
4da755e75f | ||
|
|
bd7aee7c1f | ||
|
|
f3aef37163 | ||
|
|
7d262296e1 | ||
|
|
90a4b62976 | ||
|
|
7346083b26 | ||
|
|
f052bbc36e | ||
|
|
8d4329197a | ||
|
|
cb83eb204b | ||
|
|
74d525364a | ||
|
|
125975832b | ||
|
|
bcf22831e2 | ||
|
|
3b26ce6501 | ||
|
|
1b2d3bf08b | ||
|
|
492bc9f86e | ||
|
|
967feb6931 | ||
|
|
f224ad2959 | ||
|
|
242cb7c7cb | ||
|
|
ea7386b04b | ||
|
|
7a27dbb374 | ||
|
|
a85e6370a8 | ||
|
|
09a03565d7 | ||
|
|
6159994552 | ||
|
|
a1f624c1cc | ||
|
|
328958876a | ||
|
|
68f73c7f94 | ||
|
|
ec4d28ac6c | ||
|
|
957074a134 | ||
|
|
c4f7e8121a | ||
|
|
6436d703f5 | ||
|
|
ec0cb7a8bc | ||
|
|
e98f0c39d1 | ||
|
|
50a451eddc | ||
|
|
a5a7358d26 | ||
|
|
f9452163c5 | ||
|
|
3067c3f262 | ||
|
|
7a64404299 | ||
|
|
2bda399982 | ||
|
|
74731bc6ae | ||
|
|
7cb287d6c6 | ||
|
|
aa8f734bd1 | ||
|
|
f6d1163ddd | ||
|
|
5be30bd278 | ||
|
|
fa7b7288c9 | ||
|
|
9cc03aaa9a | ||
|
|
1bda56ea23 | ||
|
|
64a34ced72 | ||
|
|
e05d379101 | ||
|
|
a355783377 | ||
|
|
88239e0b0d | ||
|
|
5c63a499d5 | ||
|
|
50496b1a59 | ||
|
|
f7b0d22f86 | ||
|
|
ad95b86fdd | ||
|
|
43e1e0dbc8 | ||
|
|
f731900e2f | ||
|
|
b1bcaa33e7 | ||
|
|
17873706b7 | ||
|
|
e0ad2b4555 | ||
|
|
f89d91783b | ||
|
|
3ffe36e5ed | ||
|
|
be393a9d10 | ||
|
|
27eefd8705 | ||
|
|
097e0f38ff | ||
|
|
ce26b566a4 | ||
|
|
0e14bc1e02 | ||
|
|
ce6796ed9b | ||
|
|
c90cecc2fb | ||
|
|
b6bbcb0609 | ||
|
|
23f6832d9c | ||
|
|
88dace75a1 | ||
|
|
8eb140fd65 | ||
|
|
1f09f3d096 | ||
|
|
66be85a41f | ||
|
|
814c11167e | ||
|
|
57ddd5086f | ||
|
|
c171547037 |
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -17,15 +17,20 @@ about: Report a reproducible bug in the current release of NetBox
|
||||
-->
|
||||
### Environment
|
||||
* Python version: <!-- Example: 3.5.4 -->
|
||||
* NetBox version: <!-- Example: 2.3.6 -->
|
||||
* NetBox version: <!-- Example: 2.5.2 -->
|
||||
|
||||
<!--
|
||||
Describe in detail the steps that someone else can take to reproduce this
|
||||
bug using the current stable release of NetBox (or the current beta release
|
||||
where applicable).
|
||||
Describe in detail the exact steps that someone else can take to reproduce
|
||||
this bug using the current stable release of NetBox (or the current beta
|
||||
release where applicable). Begin with the creation of any necessary
|
||||
database objects and call out every operation being performed explicitly.
|
||||
If reporting a bug in the REST API, be sure to reconstruct the raw HTTP
|
||||
request(s) being made: Don't rely on a wrapper like pynetbox.
|
||||
-->
|
||||
### Steps to Reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
<!-- What did you expect to happen? -->
|
||||
### Expected Behavior
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,3 +10,5 @@
|
||||
fabfile.py
|
||||
*.swp
|
||||
gunicorn_config.py
|
||||
.DS_Store
|
||||
.vscode
|
||||
|
||||
3834
CHANGELOG.md
3834
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@ and run `upgrade.sh`.
|
||||
|
||||
## Alternative Installations
|
||||
|
||||
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
||||
* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
||||
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
|
||||
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
|
||||
|
||||
|
||||
@@ -1,19 +1,72 @@
|
||||
# The Python web framework on which NetBox is built
|
||||
# https://github.com/django/django
|
||||
Django
|
||||
|
||||
# Django middleware which permits cross-domain API requests
|
||||
# https://github.com/OttoYiu/django-cors-headers
|
||||
django-cors-headers
|
||||
|
||||
# Runtime UI tool for debugging Django
|
||||
# https://github.com/jazzband/django-debug-toolbar
|
||||
django-debug-toolbar
|
||||
|
||||
# Library for writing reusable URL query filters
|
||||
# https://github.com/carltongibson/django-filter
|
||||
django-filter
|
||||
|
||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||
# https://github.com/django-mptt/django-mptt
|
||||
django-mptt
|
||||
|
||||
# Abstraction models for rendering and paginating HTML tables
|
||||
# https://github.com/jieter/django-tables2
|
||||
django-tables2
|
||||
|
||||
# User-defined tags for objects
|
||||
# https://github.com/alex/django-taggit
|
||||
django-taggit
|
||||
|
||||
# A Django REST Framework serializer which represents tags
|
||||
# https://github.com/glemmaPaul/django-taggit-serializer
|
||||
django-taggit-serializer
|
||||
|
||||
# A Django field for representing time zones
|
||||
# https://github.com/mfogel/django-timezone-field/
|
||||
django-timezone-field
|
||||
|
||||
# A REST API framework for Django projects
|
||||
# https://github.com/encode/django-rest-framework
|
||||
djangorestframework
|
||||
|
||||
# Swagger/OpenAPI schema generation for REST APIs
|
||||
# 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
|
||||
Markdown<3.0
|
||||
|
||||
# Library for manipulating IP prefixes and addresses
|
||||
# https://github.com/drkjam/netaddr
|
||||
netaddr
|
||||
|
||||
# Fork of PIL (Python Imaging Library) for image processing
|
||||
# https://github.com/python-pillow/Pillow
|
||||
Pillow
|
||||
|
||||
# PostgreSQL database adapter for Python
|
||||
# https://github.com/psycopg/psycopg2
|
||||
psycopg2-binary
|
||||
|
||||
# GitHub-flavored Markdown extensions
|
||||
# https://github.com/zopieux/py-gfm
|
||||
py-gfm
|
||||
|
||||
# Extensive cryptographic library (fork of pycrypto)
|
||||
# https://github.com/Legrandin/pycryptodome
|
||||
pycryptodome
|
||||
|
||||
@@ -44,7 +44,7 @@ class DeviceConnectionsReport(Report):
|
||||
|
||||
# Check that every console port for every active device has a connection defined.
|
||||
for console_port in ConsolePort.objects.select_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
|
||||
if console_port.cs_port is None:
|
||||
if console_port.connected_endpoint is None:
|
||||
self.log_failure(
|
||||
console_port.device,
|
||||
"No console connection defined for {}".format(console_port.name)
|
||||
@@ -63,7 +63,7 @@ class DeviceConnectionsReport(Report):
|
||||
for device in Device.objects.filter(status=DEVICE_STATUS_ACTIVE):
|
||||
connected_ports = 0
|
||||
for power_port in PowerPort.objects.filter(device=device):
|
||||
if power_port.power_outlet is not None:
|
||||
if power_port.connected_endpoint is not None:
|
||||
connected_ports += 1
|
||||
if power_port.connection_status == CONNECTION_STATUS_PLANNED:
|
||||
self.log_warning(
|
||||
@@ -128,4 +128,4 @@ Reports can be run on the CLI by invoking the management command:
|
||||
python3 manage.py runreport <module>
|
||||
```
|
||||
|
||||
One or more report modules may be specified.
|
||||
where ``<module>`` is the name of the python file in the ``reports`` directory without the ``.py`` extension. One or more report modules may be specified.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Tags
|
||||
|
||||
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object 9for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
|
||||
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object (for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
|
||||
|
||||
Each tag has a label and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters.
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ The base serializer is used to represent the default view of a model. This inclu
|
||||
}
|
||||
```
|
||||
|
||||
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
|
||||
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
|
||||
|
||||
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
|
||||
|
||||
@@ -122,6 +122,52 @@ When a base serializer includes one or more nested serializers, the hierarchical
|
||||
}
|
||||
```
|
||||
|
||||
## Brief Format
|
||||
|
||||
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of the objects themselves without any related data, such as when populating a drop-down list in a form.
|
||||
|
||||
For example, the default (complete) format of an IP address looks like this:
|
||||
|
||||
```
|
||||
GET /api/ipam/prefixes/13980/
|
||||
|
||||
{
|
||||
"id": 13980,
|
||||
"family": 4,
|
||||
"prefix": "192.0.2.0/24",
|
||||
"site": null,
|
||||
"vrf": null,
|
||||
"tenant": null,
|
||||
"vlan": null,
|
||||
"status": {
|
||||
"value": 1,
|
||||
"label": "Active"
|
||||
},
|
||||
"role": null,
|
||||
"is_pool": false,
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"custom_fields": {},
|
||||
"created": "2018-12-11",
|
||||
"last_updated": "2018-12-11T16:27:55.073174-05:00"
|
||||
}
|
||||
```
|
||||
|
||||
The brief format is much more terse, but includes a link to the object's full representation:
|
||||
|
||||
```
|
||||
GET /api/ipam/prefixes/13980/?brief=1
|
||||
|
||||
{
|
||||
"id": 13980,
|
||||
"url": "https://netbox/api/ipam/prefixes/13980/",
|
||||
"family": 4,
|
||||
"prefix": "192.0.2.0/24"
|
||||
}
|
||||
```
|
||||
|
||||
The brief format is supported for both lists and individual objects.
|
||||
|
||||
## Static Choice Fields
|
||||
|
||||
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.
|
||||
|
||||
@@ -251,7 +251,7 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
|
||||
|
||||
Default: False
|
||||
|
||||
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../miscellaneous/webhooks/) for more information on setup and use.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -274,7 +274,7 @@ SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-27 13:23
|
||||
|
||||
## Redis Connection Settings
|
||||
|
||||
[Redis](https://redis.io/) is a key-value store which functions as a very lightweight database. It is required when enabling NetBox [webhooks](../miscellaneous/webhooks/). A Redis connection is configured using a dictionary similar to the following:
|
||||
[Redis](https://redis.io/) is a key-value store which functions as a very lightweight database. It is required when enabling NetBox [webhooks](../additional-features/webhooks/). A Redis connection is configured using a dictionary similar to the following:
|
||||
|
||||
```
|
||||
REDIS = {
|
||||
|
||||
@@ -13,6 +13,10 @@ Some devices house child devices which share physical resources, like space and
|
||||
!!! note
|
||||
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane.
|
||||
|
||||
For that application you should create a single Device for the chassis, and add Interfaces directly to it. Interfaces can be created in bulk using range patterns, e.g. "Gi1/[1-24]".
|
||||
|
||||
Add Inventory Items if you want to record the line cards themselves as separate entities. There is no explicit relationship between each interface and its line card, but it may be implied by the naming (e.g. interfaces "Gi1/x" are on line card 1)
|
||||
|
||||
## Manufacturers
|
||||
|
||||
Each device type must be assigned to a manufacturer. The model number of a device type must be unique to its manufacturer.
|
||||
@@ -93,6 +97,10 @@ Pass-through ports can also be used to model "bump in the wire" devices, such as
|
||||
|
||||
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
|
||||
|
||||
Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices.
|
||||
|
||||
Therefore, Device bays are **not** suitable for modeling chassis-based switches and routers. These should instead be modeled as a single Device, with the line cards as Inventory Items.
|
||||
|
||||
## Device Roles
|
||||
|
||||
Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches.
|
||||
@@ -111,7 +119,7 @@ The assignment of platforms to devices is an optional feature, and may be disreg
|
||||
|
||||
# Inventory Items
|
||||
|
||||
Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
|
||||
Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ An IP address can be designated as the network address translation (NAT) inside
|
||||
|
||||
A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space).
|
||||
|
||||
Each VRF is assigned a unique name and route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
|
||||
Each VRF is assigned a unique name and an optional route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
|
||||
|
||||
Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
|
||||
|
||||
|
||||
@@ -44,7 +44,11 @@ If you're adding a relational field (e.g. `ForeignKey`) and intend to include th
|
||||
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model.
|
||||
|
||||
### 6. Add field to forms
|
||||
### 6. Add choices to API view
|
||||
|
||||
If the new field has static choices, add it to the `FieldChoicesViewSet` for the app.
|
||||
|
||||
### 7. Add field to forms
|
||||
|
||||
Extend any forms to include the new field as appropriate. Common forms include:
|
||||
|
||||
@@ -53,18 +57,18 @@ Extend any forms to include the new field as appropriate. Common forms include:
|
||||
* **CSV import** - The form used when bulk importing objects in CSV format
|
||||
* **Filter** - Displays the options available for filtering a list of objects (both UI and API)
|
||||
|
||||
### 7. Extend object filter set
|
||||
### 8. Extend object filter set
|
||||
|
||||
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method.
|
||||
|
||||
### 8. Add column to object table
|
||||
### 9. Add column to object table
|
||||
|
||||
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require explicitly declaring a new column.
|
||||
|
||||
### 9. Update the UI templates
|
||||
### 10. Update the UI templates
|
||||
|
||||
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.
|
||||
|
||||
### 10. Adjust API and model tests
|
||||
### 11. Adjust API and model tests
|
||||
|
||||
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.
|
||||
|
||||
@@ -28,6 +28,19 @@ To invoke `pycodestyle` manually, run:
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
```
|
||||
|
||||
## Introducing New Dependencies
|
||||
|
||||
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and attacks.
|
||||
|
||||
If there's a strong case for introducing a new depdency, it must meet the following criteria:
|
||||
|
||||
* Its complete source code must be published and freely accessible without registration.
|
||||
* Its license must be conducive to inclusion in an open source project.
|
||||
* It must be actively maintained, with no longer than one year between releases.
|
||||
* It must be available via the [Python Package Index](https://pypi.org/) (PyPI).
|
||||
|
||||
When adding a new dependency, a short description of the package and the URL of its code repository must be added to `base_requirements.txt`. Additionally, a line specifying the package name pinned to the current stable release must be added to `requirements.txt`. This ensures that NetBox will install only the known-good release and simplify support efforts.
|
||||
|
||||
## General Guidance
|
||||
|
||||
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
NetBox requires a PostgreSQL database to store data. This can be hosted locally or on a remote server. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/current/static/datatype-net-types.html).)
|
||||
|
||||
!!! note
|
||||
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 7.4. 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.
|
||||
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 v2.2 and later requires PostgreSQL 9.4 or higher.
|
||||
@@ -19,7 +19,7 @@ If a recent enough version of PostgreSQL is not available through your distribut
|
||||
|
||||
**CentOS**
|
||||
|
||||
CentOS 7.4 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6.
|
||||
CentOS 7.5 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6.
|
||||
|
||||
```no-highlight
|
||||
# yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm
|
||||
|
||||
@@ -5,16 +5,16 @@ This section of the documentation discusses installing and configuring the NetBo
|
||||
**Ubuntu**
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python3 python3-dev python3-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
|
||||
# easy_install3 pip
|
||||
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
|
||||
```
|
||||
|
||||
**CentOS**
|
||||
|
||||
```no-highlight
|
||||
# yum install -y epel-release
|
||||
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
|
||||
# easy_install-3.4 pip
|
||||
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
|
||||
# easy_install-3.6 pip
|
||||
# ln -s /usr/bin/python36 /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.
|
||||
@@ -246,13 +246,13 @@ At this point, NetBox should be able to run. We can verify this by starting a de
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
June 17, 2016 - 16:17:36
|
||||
Django version 1.9.7, using settings 'netbox.settings'
|
||||
November 28, 2018 - 09:33:45
|
||||
Django version 2.0.9, using settings 'netbox.settings'
|
||||
Starting development server at http://0.0.0.0:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
```
|
||||
|
||||
Now if we navigate to the name or IP of the server (as defined in `ALLOWED_HOSTS`) we should be greeted with the NetBox home page. Note that this built-in web service is for development and testing purposes only. **It is not suited for production use.**
|
||||
Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Note that this built-in web service is for development and testing purposes only. **It is not suited for production use.**
|
||||
|
||||
!!! 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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
|
||||
|
||||
!!! info
|
||||
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
|
||||
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.
|
||||
|
||||
# Web Server Installation
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ sudo yum install -y openldap-devel
|
||||
## Install django-auth-ldap
|
||||
|
||||
```no-highlight
|
||||
sudo pip install django-auth-ldap
|
||||
pip3 install django-auth-ldap
|
||||
```
|
||||
|
||||
# Configuration
|
||||
@@ -95,6 +95,9 @@ AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
|
||||
# Define a group required to login.
|
||||
AUTH_LDAP_REQUIRE_GROUP = "CN=NETBOX_USERS,DC=example,DC=com"
|
||||
|
||||
# Mirror LDAP group assignments.
|
||||
AUTH_LDAP_MIRROR_GROUPS = True
|
||||
|
||||
# Define special user types using groups. Exercise great caution when assigning superuser status.
|
||||
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
|
||||
"is_active": "cn=active,ou=groups,dc=example,dc=com",
|
||||
@@ -113,3 +116,21 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
|
||||
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
|
||||
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
|
||||
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
|
||||
|
||||
# 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/`.
|
||||
|
||||
For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`.
|
||||
|
||||
```python
|
||||
import logging, logging.handlers
|
||||
logfile = "/opt/netbox/logs/django-ldap-debug.log"
|
||||
my_logger = logging.getLogger('django_auth_ldap')
|
||||
my_logger.setLevel(logging.DEBUG)
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
logfile, maxBytes=1024 * 500, backupCount=5)
|
||||
my_logger.addHandler(handler)
|
||||
```
|
||||
|
||||
Ensure the file and path specified in logfile exist and are writable and executable by the application service account. Restart the netbox service and attempt to log into the site to trigger log entries to this file.
|
||||
|
||||
@@ -11,4 +11,4 @@ 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. Please see the instruction for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.
|
||||
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.
|
||||
|
||||
@@ -36,3 +36,9 @@ If using LDAP authentication, install the `django-auth-ldap` package:
|
||||
```no-highlight
|
||||
# pip3 install django-auth-ldap
|
||||
```
|
||||
|
||||
If using Webhooks, install the `django-rq` package:
|
||||
|
||||
```no-highlight
|
||||
# pip3 install django-rq
|
||||
```
|
||||
|
||||
@@ -38,6 +38,7 @@ pages:
|
||||
- Change Logging: 'additional-features/change-logging.md'
|
||||
- Administration:
|
||||
- Replicating NetBox: 'administration/replicating-netbox.md'
|
||||
- NetBox Shell: 'administration/netbox-shell.md'
|
||||
- API:
|
||||
- Overview: 'api/overview.md'
|
||||
- Authentication: 'api/authentication.md'
|
||||
|
||||
@@ -60,5 +60,5 @@ class CircuitTerminationSerializer(ConnectedEndpointSerializer):
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description', 'connected_endpoint', 'connection_status', 'cable',
|
||||
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
|
||||
]
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.db.models import Q
|
||||
from dcim.models import Site
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||
from .constants import CIRCUIT_STATUS_CHOICES
|
||||
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
|
||||
@@ -47,7 +47,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeFilter(django_filters.FilterSet):
|
||||
class CircuitTypeFilter(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
from taggit.forms import TagField
|
||||
|
||||
from dcim.models import Site
|
||||
@@ -7,8 +6,8 @@ from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEdit
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
AnnotatedMultipleChoiceField, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField,
|
||||
SmallTextarea, SlugField,
|
||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
|
||||
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
|
||||
)
|
||||
from .constants import CIRCUIT_STATUS_CHOICES
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
@@ -21,14 +20,22 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
class ProviderForm(BootstrapMixin, CustomFieldForm):
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
tags = TagField(required=False)
|
||||
tags = TagField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags']
|
||||
fields = [
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'noc_contact': SmallTextarea(attrs={'rows': 5}),
|
||||
'admin_contact': SmallTextarea(attrs={'rows': 5}),
|
||||
'noc_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
),
|
||||
'admin_contact': SmallTextarea(
|
||||
attrs={'rows': 5}
|
||||
),
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the provider",
|
||||
@@ -54,23 +61,61 @@ class ProviderCSVForm(forms.ModelForm):
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
asn = forms.IntegerField(required=False, label='ASN')
|
||||
account = forms.CharField(max_length=30, required=False, label='Account number')
|
||||
portal_url = forms.URLField(required=False, label='Portal')
|
||||
noc_contact = forms.CharField(required=False, widget=SmallTextarea, label='NOC contact')
|
||||
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
|
||||
comments = CommentField(widget=SmallTextarea)
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label='ASN'
|
||||
)
|
||||
account = forms.CharField(
|
||||
max_length=30,
|
||||
required=False,
|
||||
label='Account number'
|
||||
)
|
||||
portal_url = forms.URLField(
|
||||
required=False,
|
||||
label='Portal'
|
||||
)
|
||||
noc_contact = forms.CharField(
|
||||
required=False,
|
||||
widget=SmallTextarea,
|
||||
label='NOC contact'
|
||||
)
|
||||
admin_contact = forms.CharField(
|
||||
required=False,
|
||||
widget=SmallTextarea,
|
||||
label='Admin contact'
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
nullable_fields = [
|
||||
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Provider
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
|
||||
asn = forms.IntegerField(required=False, label='ASN')
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label='ASN'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -82,7 +127,9 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['name', 'slug']
|
||||
fields = [
|
||||
'name', 'slug',
|
||||
]
|
||||
|
||||
|
||||
class CircuitTypeCSVForm(forms.ModelForm):
|
||||
@@ -102,7 +149,9 @@ class CircuitTypeCSVForm(forms.ModelForm):
|
||||
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
comments = CommentField()
|
||||
tags = TagField(required=False)
|
||||
tags = TagField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
@@ -115,6 +164,16 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
'install_date': "Format: YYYY-MM-DD",
|
||||
'commit_rate': "Committed rate",
|
||||
}
|
||||
widgets = {
|
||||
'provider': APISelect(
|
||||
api_url="/api/circuits/providers/"
|
||||
),
|
||||
'type': APISelect(
|
||||
api_url="/api/circuits/circuit-types/"
|
||||
),
|
||||
'status': StaticSelect2(),
|
||||
|
||||
}
|
||||
|
||||
|
||||
class CircuitCSVForm(forms.ModelForm):
|
||||
@@ -157,46 +216,105 @@ class CircuitCSVForm(forms.ModelForm):
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(CIRCUIT_STATUS_CHOICES), required=False, initial='')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
comments = CommentField(widget=SmallTextarea)
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
type = forms.ModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/circuits/circuit-types/"
|
||||
)
|
||||
)
|
||||
provider = forms.ModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/circuits/providers/"
|
||||
)
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(CIRCUIT_STATUS_CHOICES),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/tenancy/tenants/"
|
||||
)
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
label='Commit rate (Kbps)'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
widget=SmallTextarea
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'commit_rate', 'description', 'comments']
|
||||
nullable_fields = [
|
||||
'tenant', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Circuit
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
type = FilterChoiceField(
|
||||
queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug'
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/circuits/circuit-types/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
provider = FilterChoiceField(
|
||||
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug'
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/circuits/providers/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
status = AnnotatedMultipleChoiceField(
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=CIRCUIT_STATUS_CHOICES,
|
||||
annotate=Circuit.objects.all(),
|
||||
annotate_field='status',
|
||||
required=False
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --'
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||
to_field_name='slug'
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
commit_rate = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=0,
|
||||
label='Commit rate (Kbps)'
|
||||
)
|
||||
commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)')
|
||||
|
||||
|
||||
#
|
||||
@@ -217,4 +335,7 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
widgets = {
|
||||
'term_side': forms.HiddenInput(),
|
||||
'site': APISelect(
|
||||
api_url="/api/dcim/sites/"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, STATUS_CLASSES
|
||||
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
|
||||
@@ -176,7 +176,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
unique_together = ['provider', 'cid']
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(self.provider, self.cid)
|
||||
return self.cid
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
@@ -283,6 +283,10 @@ class CircuitTermination(CableTermination):
|
||||
object_data=serialize_object(self)
|
||||
).save()
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.circuit
|
||||
|
||||
def get_peer_termination(self):
|
||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||
try:
|
||||
|
||||
@@ -59,7 +59,7 @@ 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'}}, verbose_name=''
|
||||
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
||||
@@ -13,7 +13,7 @@ class ProviderTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ProviderTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
|
||||
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
|
||||
@@ -135,7 +135,7 @@ class CircuitTypeTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(CircuitTypeTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
|
||||
self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
|
||||
@@ -210,7 +210,7 @@ class CircuitTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(CircuitTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
|
||||
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
|
||||
@@ -326,7 +326,7 @@ class CircuitTerminationTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(CircuitTerminationTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
|
||||
91
netbox/circuits/tests/test_views.py
Normal file
91
netbox/circuits/tests/test_views.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import urllib.parse
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from circuits.models import Circuit, CircuitType, Provider
|
||||
|
||||
|
||||
class ProviderTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Provider.objects.bulk_create([
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65001),
|
||||
Provider(name='Provider 2', slug='provider-2', asn=65002),
|
||||
Provider(name='Provider 3', slug='provider-3', asn=65003),
|
||||
])
|
||||
|
||||
def test_provider_list(self):
|
||||
|
||||
url = reverse('circuits:provider_list')
|
||||
params = {
|
||||
"q": "test",
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_provider(self):
|
||||
|
||||
provider = Provider.objects.first()
|
||||
response = self.client.get(provider.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class CircuitTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
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_circuittype_list(self):
|
||||
|
||||
url = reverse('circuits:circuittype_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class CircuitTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
|
||||
provider.save()
|
||||
|
||||
circuittype = CircuitType(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuittype.save()
|
||||
|
||||
Circuit.objects.bulk_create([
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
])
|
||||
|
||||
def test_circuit_list(self):
|
||||
|
||||
url = reverse('circuits:circuit_list')
|
||||
params = {
|
||||
"provider": Provider.objects.first().slug,
|
||||
"type": CircuitType.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_provider(self):
|
||||
|
||||
provider = Provider.objects.first()
|
||||
response = self.client.get(provider.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1,11 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
|
||||
Interface, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, RearPort, RearPortTemplate,
|
||||
Region, Site, VirtualChassis,
|
||||
)
|
||||
from utilities.api import WritableNestedSerializer
|
||||
from utilities.api import ChoiceField, WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCableSerializer',
|
||||
@@ -99,7 +100,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
@@ -149,46 +150,51 @@ class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
|
||||
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
|
||||
@@ -23,9 +23,18 @@ from .nested_serializers import *
|
||||
|
||||
|
||||
class ConnectedEndpointSerializer(ValidatedModelSerializer):
|
||||
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
|
||||
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
def get_connected_endpoint_type(self, obj):
|
||||
if hasattr(obj, 'connected_endpoint') and obj.connected_endpoint is not None:
|
||||
return '{}.{}'.format(
|
||||
obj.connected_endpoint._meta.app_label,
|
||||
obj.connected_endpoint._meta.model_name
|
||||
)
|
||||
return None
|
||||
|
||||
def get_connected_endpoint(self, obj):
|
||||
"""
|
||||
Return the appropriate serializer for the type of connected object.
|
||||
@@ -58,6 +67,11 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
time_zone = TimeZoneField(required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
count_prefixes = serializers.IntegerField(read_only=True)
|
||||
count_vlans = serializers.IntegerField(read_only=True)
|
||||
count_racks = serializers.IntegerField(read_only=True)
|
||||
count_devices = serializers.IntegerField(read_only=True)
|
||||
count_circuits = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
@@ -121,7 +135,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
validator(data)
|
||||
|
||||
# Enforce model validation
|
||||
super(RackSerializer, self).validate(data)
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
@@ -166,8 +180,8 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
|
||||
'id', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
|
||||
'subdevice_role', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -294,7 +308,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
validator(data)
|
||||
|
||||
# Enforce model validation
|
||||
super(DeviceSerializer, self).validate(data)
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
@@ -331,7 +345,10 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
fields = [
|
||||
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
|
||||
'tags',
|
||||
]
|
||||
|
||||
|
||||
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
@@ -341,7 +358,10 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
fields = [
|
||||
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
|
||||
'tags',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
@@ -351,7 +371,10 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
fields = [
|
||||
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
|
||||
'tags',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
@@ -361,7 +384,10 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
fields = [
|
||||
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
|
||||
'tags',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
@@ -383,8 +409,8 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
|
||||
'count_ipaddresses',
|
||||
'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()
|
||||
@@ -405,7 +431,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
"be global.".format(vlan)
|
||||
})
|
||||
|
||||
return super(InterfaceSerializer, self).validate(data)
|
||||
return super().validate(data)
|
||||
|
||||
|
||||
class RearPortSerializer(ValidatedModelSerializer):
|
||||
@@ -481,7 +507,7 @@ class CableSerializer(ValidatedModelSerializer):
|
||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||
status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
|
||||
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False)
|
||||
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
|
||||
@@ -35,12 +35,18 @@ from .exceptions import MissingFilterException
|
||||
|
||||
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Device, ['face', 'status']),
|
||||
(Cable, ['length_unit', 'status', 'type']),
|
||||
(ConsolePort, ['connection_status']),
|
||||
(Interface, ['connection_status', 'form_factor', 'mode']),
|
||||
(Device, ['face', 'status']),
|
||||
(DeviceType, ['subdevice_role']),
|
||||
(FrontPort, ['type']),
|
||||
(FrontPortTemplate, ['type']),
|
||||
(Interface, ['form_factor', 'mode']),
|
||||
(InterfaceTemplate, ['form_factor']),
|
||||
(PowerPort, ['connection_status']),
|
||||
(Rack, ['type', 'width']),
|
||||
(Rack, ['outer_unit', 'status', 'type', 'width']),
|
||||
(RearPort, ['type']),
|
||||
(RearPortTemplate, ['type']),
|
||||
(Site, ['status']),
|
||||
)
|
||||
|
||||
@@ -59,7 +65,7 @@ class CableTraceMixin(object):
|
||||
# Initialize the path array
|
||||
path = []
|
||||
|
||||
for near_end, cable, far_end in obj.trace():
|
||||
for near_end, cable, far_end in obj.trace(follow_circuits=True):
|
||||
|
||||
# Serialize each object
|
||||
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
|
||||
@@ -153,6 +159,11 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
exclude_pk = None
|
||||
elevation = rack.get_rack_units(face, exclude_pk)
|
||||
|
||||
# Enable filtering rack units by ID
|
||||
q = request.GET.get('q', None)
|
||||
if q:
|
||||
elevation = [u for u in elevation if q in str(u['id'])]
|
||||
|
||||
page = self.paginate_queryset(elevation)
|
||||
if page is not None:
|
||||
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
|
||||
@@ -483,13 +494,13 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
filterset_class = filters.PowerConnectionFilter
|
||||
|
||||
|
||||
class InterfaceConnectionViewSet(ModelViewSet):
|
||||
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = Interface.objects.select_related(
|
||||
'device', '_connected_interface', '_connected_circuittermination'
|
||||
'device', '_connected_interface__device'
|
||||
).filter(
|
||||
# Avoid duplicate connections by only selecting the lower PK in a connected pair
|
||||
Q(_connected_interface__isnull=False, pk__lt=F('_connected_interface')) |
|
||||
Q(_connected_circuittermination__isnull=False)
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
)
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
filterset_class = filters.InterfaceConnectionFilter
|
||||
|
||||
@@ -43,6 +43,12 @@ RACK_STATUS_CHOICES = [
|
||||
[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
|
||||
@@ -82,18 +88,35 @@ IFACE_FF_100GE_CFP2 = 1510
|
||||
IFACE_FF_100GE_CFP4 = 1520
|
||||
IFACE_FF_100GE_CPAK = 1550
|
||||
IFACE_FF_100GE_QSFP28 = 1600
|
||||
IFACE_FF_200GE_CFP2 = 1650
|
||||
IFACE_FF_200GE_QSFP56 = 1700
|
||||
IFACE_FF_400GE_QSFP_DD = 1750
|
||||
# Wireless
|
||||
IFACE_FF_80211A = 2600
|
||||
IFACE_FF_80211G = 2610
|
||||
IFACE_FF_80211N = 2620
|
||||
IFACE_FF_80211AC = 2630
|
||||
IFACE_FF_80211AD = 2640
|
||||
# Cellular
|
||||
IFACE_FF_GSM = 2810
|
||||
IFACE_FF_CDMA = 2820
|
||||
IFACE_FF_LTE = 2830
|
||||
# SONET
|
||||
IFACE_FF_SONET_OC3 = 6100
|
||||
IFACE_FF_SONET_OC12 = 6200
|
||||
IFACE_FF_SONET_OC48 = 6300
|
||||
IFACE_FF_SONET_OC192 = 6400
|
||||
IFACE_FF_SONET_OC768 = 6500
|
||||
IFACE_FF_SONET_OC1920 = 6600
|
||||
IFACE_FF_SONET_OC3840 = 6700
|
||||
# Fibrechannel
|
||||
IFACE_FF_1GFC_SFP = 3010
|
||||
IFACE_FF_2GFC_SFP = 3020
|
||||
IFACE_FF_4GFC_SFP = 3040
|
||||
IFACE_FF_8GFC_SFP_PLUS = 3080
|
||||
IFACE_FF_16GFC_SFP_PLUS = 3160
|
||||
IFACE_FF_32GFC_SFP28 = 3320
|
||||
IFACE_FF_128GFC_QSFP28 = 3400
|
||||
# Serial
|
||||
IFACE_FF_T1 = 4000
|
||||
IFACE_FF_E1 = 4010
|
||||
@@ -143,9 +166,12 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
|
||||
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
|
||||
[IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'],
|
||||
[IFACE_FF_100GE_CFP4, 'CFP4 (100GE)'],
|
||||
[IFACE_FF_100GE_CPAK, 'Cisco CPAK (100GE)'],
|
||||
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
|
||||
[IFACE_FF_200GE_QSFP56, 'QSFP56 (200GE)'],
|
||||
[IFACE_FF_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -158,6 +184,26 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_80211AD, 'IEEE 802.11ad'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Cellular',
|
||||
[
|
||||
[IFACE_FF_GSM, 'GSM'],
|
||||
[IFACE_FF_CDMA, 'CDMA'],
|
||||
[IFACE_FF_LTE, 'LTE'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'SONET',
|
||||
[
|
||||
[IFACE_FF_SONET_OC3, 'OC-3/STM-1'],
|
||||
[IFACE_FF_SONET_OC12, 'OC-12/STM-4'],
|
||||
[IFACE_FF_SONET_OC48, 'OC-48/STM-16'],
|
||||
[IFACE_FF_SONET_OC192, 'OC-192/STM-64'],
|
||||
[IFACE_FF_SONET_OC768, 'OC-768/STM-256'],
|
||||
[IFACE_FF_SONET_OC1920, 'OC-1920/STM-640'],
|
||||
[IFACE_FF_SONET_OC3840, 'OC-3840/STM-1234'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'FibreChannel',
|
||||
[
|
||||
@@ -166,6 +212,8 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
|
||||
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
|
||||
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
|
||||
[IFACE_FF_32GFC_SFP28, 'SFP28 (32GFC)'],
|
||||
[IFACE_FF_128GFC_QSFP28, 'QSFP28 (128GFC)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -225,18 +273,23 @@ IFACE_MODE_CHOICES = [
|
||||
|
||||
# Pass-through port types
|
||||
PORT_TYPE_8P8C = 1000
|
||||
PORT_TYPE_110_PUNCH = 1100
|
||||
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'],
|
||||
],
|
||||
],
|
||||
[
|
||||
@@ -244,10 +297,13 @@ PORT_TYPE_CHOICES = [
|
||||
[
|
||||
[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'],
|
||||
]
|
||||
]
|
||||
@@ -309,11 +365,17 @@ 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_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 = (
|
||||
(
|
||||
@@ -324,15 +386,21 @@ CABLE_TYPE_CHOICES = (
|
||||
(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)'),
|
||||
),
|
||||
),
|
||||
(
|
||||
'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'),
|
||||
@@ -360,11 +428,11 @@ COMPATIBLE_TERMINATION_TYPES = {
|
||||
'circuittermination': ['interface', 'frontport', 'rearport'],
|
||||
}
|
||||
|
||||
LENGTH_UNIT_METER = 'm'
|
||||
LENGTH_UNIT_CENTIMETER = 'cm'
|
||||
LENGTH_UNIT_MILLIMETER = 'mm'
|
||||
LENGTH_UNIT_FOOT = 'ft'
|
||||
LENGTH_UNIT_INCH = 'in'
|
||||
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'),
|
||||
|
||||
5
netbox/dcim/exceptions.py
Normal file
5
netbox/dcim/exceptions.py
Normal file
@@ -0,0 +1,5 @@
|
||||
class LoopDetected(Exception):
|
||||
"""
|
||||
A loop has been detected while tracing a cable path.
|
||||
"""
|
||||
pass
|
||||
@@ -1,13 +1,15 @@
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Q
|
||||
from netaddr import EUI
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter
|
||||
from utilities.constants import COLOR_CHOICES
|
||||
from utilities.filters import (
|
||||
NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .constants import *
|
||||
from .models import (
|
||||
@@ -18,11 +20,7 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
class RegionFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class RegionFilter(NameSlugSearchFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label='Parent region (ID)',
|
||||
@@ -38,15 +36,6 @@ class RegionFilter(django_filters.FilterSet):
|
||||
model = Region
|
||||
fields = ['name', 'slug']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(slug__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(
|
||||
@@ -61,13 +50,14 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
choices=SITE_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='region__slug',
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -108,11 +98,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class RackGroupFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
class RackGroupFilter(NameSlugSearchFilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -128,17 +114,8 @@ class RackGroupFilter(django_filters.FilterSet):
|
||||
model = RackGroup
|
||||
fields = ['site_id', 'name', 'slug']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(slug__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class RackRoleFilter(django_filters.FilterSet):
|
||||
class RackRoleFilter(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
@@ -292,7 +269,7 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ManufacturerFilter(django_filters.FilterSet):
|
||||
class ManufacturerFilter(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
@@ -382,7 +359,7 @@ class DeviceTypeFilter(CustomFieldFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
field_name='device_type_id',
|
||||
@@ -446,14 +423,14 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class DeviceRoleFilter(django_filters.FilterSet):
|
||||
class DeviceRoleFilter(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['name', 'slug', 'color', 'vm_role']
|
||||
|
||||
|
||||
class PlatformFilter(django_filters.FilterSet):
|
||||
class PlatformFilter(NameSlugSearchFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -528,14 +505,15 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
)
|
||||
name = NullableCharFieldFilter()
|
||||
asset_tag = NullableCharFieldFilter()
|
||||
region_id = django_filters.NumberFilter(
|
||||
method='filter_region',
|
||||
field_name='pk',
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = django_filters.CharFilter(
|
||||
method='filter_region',
|
||||
field_name='slug',
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -558,6 +536,10 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
position = django_filters.ChoiceFilter(
|
||||
choices=DEVICE_POSITION_CHOICES,
|
||||
null_label='Non-racked'
|
||||
)
|
||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Cluster.objects.all(),
|
||||
label='VM cluster (ID)',
|
||||
@@ -617,7 +599,7 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['serial', 'position']
|
||||
fields = ['serial', 'face']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -630,16 +612,6 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
def filter_region(self, queryset, name, value):
|
||||
try:
|
||||
region = Region.objects.get(**{name: value})
|
||||
except ObjectDoesNotExist:
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(site__region=region) |
|
||||
Q(site__region__in=region.get_descendants())
|
||||
)
|
||||
|
||||
def _mac_address(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
@@ -685,6 +657,10 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
|
||||
|
||||
class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
device_id = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
@@ -696,40 +672,70 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortFilter(DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name']
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
|
||||
class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['name']
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
|
||||
class PowerPortFilter(DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name']
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
|
||||
class PowerOutletFilter(DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['name']
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
|
||||
class InterfaceFilter(django_filters.FilterSet):
|
||||
"""
|
||||
Not using DeviceComponentFilterSet for Interfaces because we need to glean the ordering logic from the parent
|
||||
Device's DeviceType.
|
||||
Not using DeviceComponentFilterSet for Interfaces because we need to check for VirtualChassis membership.
|
||||
"""
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
field_name='name',
|
||||
@@ -740,6 +746,11 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
field_name='pk',
|
||||
label='Device (ID)',
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
type = django_filters.CharFilter(
|
||||
method='filter_type',
|
||||
label='Interface type',
|
||||
@@ -762,15 +773,26 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
method='filter_vlan',
|
||||
label='Assigned VID'
|
||||
)
|
||||
form_factor = django_filters.MultipleChoiceFilter(
|
||||
choices=IFACE_FF_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
|
||||
fields = ['name', 'connection_status', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value)
|
||||
).distinct()
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
try:
|
||||
device = Device.objects.get(**{name: value})
|
||||
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
|
||||
vc_interface_ids = device.vc_interfaces.values_list('id', flat=True)
|
||||
return queryset.filter(pk__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
return queryset.none()
|
||||
@@ -814,6 +836,11 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class FrontPortFilter(DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
@@ -821,6 +848,11 @@ class FrontPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
|
||||
class RearPortFilter(DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
@@ -929,6 +961,12 @@ class CableFilter(django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=CABLE_TYPE_CHOICES
|
||||
)
|
||||
color = django_filters.MultipleChoiceFilter(
|
||||
choices=COLOR_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
|
||||
1393
netbox/dcim/forms.py
1393
netbox/dcim/forms.py
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ class DeviceComponentManager(Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
queryset = super(DeviceComponentManager, self).get_queryset()
|
||||
queryset = super().get_queryset()
|
||||
table_name = self.model._meta.db_table
|
||||
sql = r"CONCAT(REGEXP_REPLACE({}.name, '\d+$', ''), LPAD(SUBSTRING({}.name FROM '\d+$'), 8, '0'))"
|
||||
|
||||
@@ -27,7 +27,7 @@ class DeviceComponentManager(Manager):
|
||||
select={
|
||||
'name_padded': sql.format(table_name, table_name),
|
||||
}
|
||||
).order_by('name_padded')
|
||||
).order_by('name_padded', 'pk')
|
||||
|
||||
|
||||
class InterfaceQuerySet(QuerySet):
|
||||
|
||||
@@ -19,11 +19,11 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[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']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[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']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[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']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[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']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -46,6 +46,9 @@ def console_connections_to_cables(apps, schema_editor):
|
||||
if 'test' not in sys.argv:
|
||||
print("{} cables created".format(cable_count))
|
||||
|
||||
# Normalize connection_status for all non-connected ConsolePorts
|
||||
ConsolePort.objects.filter(connected_endpoint__isnull=True).update(connection_status=None)
|
||||
|
||||
|
||||
def power_connections_to_cables(apps, schema_editor):
|
||||
"""
|
||||
@@ -87,6 +90,9 @@ def power_connections_to_cables(apps, schema_editor):
|
||||
if 'test' not in sys.argv:
|
||||
print("{} cables created".format(cable_count))
|
||||
|
||||
# Normalize connection_status for all non-connected PowerPorts
|
||||
PowerPort.objects.filter(connected_endpoint__isnull=True).update(connection_status=None)
|
||||
|
||||
|
||||
def interface_connections_to_cables(apps, schema_editor):
|
||||
"""
|
||||
@@ -131,6 +137,15 @@ def interface_connections_to_cables(apps, schema_editor):
|
||||
print("{} cables created".format(cable_count))
|
||||
|
||||
|
||||
def delete_interfaceconnection_content_type(apps, schema_editor):
|
||||
"""
|
||||
Delete the ContentType for the InterfaceConnection model. (This is not done automatically upon model deletion.)
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
InterfaceConnection = apps.get_model('dcim', 'InterfaceConnection')
|
||||
ContentType.objects.get_for_model(InterfaceConnection).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
@@ -157,7 +172,7 @@ class Migration(migrations.Migration):
|
||||
('label', models.CharField(blank=True, max_length=100)),
|
||||
('color', utilities.fields.ColorField(blank=True, max_length=6)),
|
||||
('length', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('length_unit', models.CharField(blank=True, max_length=2)),
|
||||
('length_unit', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('_abs_length', models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True)),
|
||||
('termination_a_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
|
||||
('termination_b_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
|
||||
@@ -291,7 +306,8 @@ class Migration(migrations.Migration):
|
||||
migrations.RunPython(power_connections_to_cables),
|
||||
migrations.RunPython(interface_connections_to_cables),
|
||||
|
||||
# Delete the InterfaceConnection model
|
||||
# Delete the InterfaceConnection model and its ContentType
|
||||
migrations.RunPython(delete_interfaceconnection_content_type),
|
||||
migrations.RemoveField(
|
||||
model_name='interfaceconnection',
|
||||
name='interface_a',
|
||||
@@ -303,36 +319,4 @@ class Migration(migrations.Migration):
|
||||
migrations.DeleteModel(
|
||||
name='InterfaceConnection',
|
||||
),
|
||||
|
||||
# Proxy models
|
||||
migrations.CreateModel(
|
||||
name='ConsoleConnection',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
},
|
||||
bases=('dcim.consoleport',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InterfaceConnection',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
},
|
||||
bases=('dcim.interface',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerConnection',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
},
|
||||
bases=('dcim.powerport',),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -28,7 +28,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='outer_unit',
|
||||
field=models.CharField(blank=True, max_length=2),
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
|
||||
38
netbox/dcim/migrations/0069_deprecate_nullablecharfield.py
Normal file
38
netbox/dcim/migrations/0069_deprecate_nullablecharfield.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 2.1.5 on 2019-02-14 14:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0068_rack_new_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='asset_tag',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=64, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='asset_tag',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='asset_tag',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='facility_id',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
]
|
||||
@@ -16,11 +16,12 @@ from taggit.managers import TaggableManager
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange
|
||||
from utilities.fields import ColorField, NullableCharField
|
||||
from utilities.fields import ColorField
|
||||
from utilities.managers import NaturalOrderingManager
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object, to_meters
|
||||
from .constants import *
|
||||
from .exceptions import LoopDetected
|
||||
from .fields import ASNField, MACAddressField
|
||||
from .managers import DeviceComponentManager, InterfaceManager
|
||||
|
||||
@@ -53,7 +54,11 @@ class ComponentModel(models.Model):
|
||||
"""
|
||||
Log an ObjectChange including the parent Device/VM.
|
||||
"""
|
||||
parent = self.device if self.device is not None else getattr(self, 'virtual_machine', None)
|
||||
try:
|
||||
parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None)
|
||||
except ObjectDoesNotExist:
|
||||
# The parent device/VM has already been deleted
|
||||
parent = None
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
@@ -63,6 +68,10 @@ class ComponentModel(models.Model):
|
||||
object_data=serialize_object(self)
|
||||
).save()
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return getattr(self, 'device', None)
|
||||
|
||||
|
||||
class CableTermination(models.Model):
|
||||
cable = models.ForeignKey(
|
||||
@@ -88,7 +97,7 @@ class CableTermination(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def trace(self, position=1, follow_circuits=False):
|
||||
def trace(self, position=1, follow_circuits=False, cable_history=None):
|
||||
"""
|
||||
Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
|
||||
[
|
||||
@@ -110,11 +119,14 @@ class CableTermination(models.Model):
|
||||
raise Exception("Invalid position for {} ({} positions): {})".format(
|
||||
termination, termination.positions, position
|
||||
))
|
||||
peer_port = FrontPort.objects.get(
|
||||
rear_port=termination,
|
||||
rear_port_position=position,
|
||||
)
|
||||
return peer_port, 1
|
||||
try:
|
||||
peer_port = FrontPort.objects.get(
|
||||
rear_port=termination,
|
||||
rear_port_position=position,
|
||||
)
|
||||
return peer_port, 1
|
||||
except ObjectDoesNotExist:
|
||||
return None, None
|
||||
|
||||
# Follow a circuit to its other termination
|
||||
elif isinstance(termination, CircuitTermination) and follow_circuits:
|
||||
@@ -130,6 +142,13 @@ class CableTermination(models.Model):
|
||||
if not self.cable:
|
||||
return [(self, None, None)]
|
||||
|
||||
# Record cable history to detect loops
|
||||
if cable_history is None:
|
||||
cable_history = []
|
||||
elif self.cable in cable_history:
|
||||
raise LoopDetected()
|
||||
cable_history.append(self.cable)
|
||||
|
||||
far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a
|
||||
path = [(self, self.cable, far_end)]
|
||||
|
||||
@@ -137,12 +156,24 @@ class CableTermination(models.Model):
|
||||
if peer_port is None:
|
||||
return path
|
||||
|
||||
next_segment = peer_port.trace(position)
|
||||
try:
|
||||
next_segment = peer_port.trace(position, follow_circuits, cable_history)
|
||||
except LoopDetected:
|
||||
return path
|
||||
|
||||
if next_segment is None:
|
||||
return path + [(peer_port, None, None)]
|
||||
|
||||
return path + next_segment
|
||||
|
||||
def get_cable_peer(self):
|
||||
if self.cable is None:
|
||||
return None
|
||||
if self._cabled_as_a.exists():
|
||||
return self.cable.termination_b
|
||||
if self._cabled_as_b.exists():
|
||||
return self.cable.termination_a
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
@@ -186,6 +217,12 @@ class Region(MPTTModel, ChangeLoggedModel):
|
||||
self.parent.name if self.parent else None,
|
||||
)
|
||||
|
||||
def get_site_count(self):
|
||||
return Site.objects.filter(
|
||||
Q(region=self) |
|
||||
Q(region__in=self.get_descendants())
|
||||
).count()
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
@@ -432,7 +469,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
facility_id = NullableCharField(
|
||||
facility_id = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -473,7 +510,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True,
|
||||
verbose_name='Serial number'
|
||||
)
|
||||
asset_tag = NullableCharField(
|
||||
asset_tag = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -511,10 +548,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
outer_unit = models.CharField(
|
||||
outer_unit = models.PositiveSmallIntegerField(
|
||||
choices=RACK_DIMENSION_UNIT_CHOICES,
|
||||
max_length=2,
|
||||
blank=True
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
@@ -544,7 +581,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super(Rack, self).__str__()
|
||||
return self.display_name or super().__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
@@ -552,10 +589,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
def clean(self):
|
||||
|
||||
# Validate outer dimensions and unit
|
||||
if (self.outer_width or self.outer_depth) and not self.outer_unit:
|
||||
if (self.outer_width is not None or self.outer_depth is not None) and self.outer_unit is None:
|
||||
raise ValidationError("Must specify a unit when setting an outer width/depth")
|
||||
else:
|
||||
self.outer_unit = ''
|
||||
elif self.outer_width is None and self.outer_depth is None:
|
||||
self.outer_unit = None
|
||||
|
||||
if self.pk:
|
||||
# Validate that Rack is tall enough to house the installed Devices
|
||||
@@ -582,7 +619,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
if self.pk:
|
||||
_site_id = Rack.objects.get(pk=self.pk).site_id
|
||||
|
||||
super(Rack, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Update racked devices if the assigned Site has been changed.
|
||||
if _site_id is not None and self.site_id != _site_id:
|
||||
@@ -894,7 +931,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
return self.model
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DeviceType, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Save a copy of u_height for validation in clean()
|
||||
self._original_u_height = self.u_height
|
||||
@@ -942,7 +979,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
def display_name(self):
|
||||
return '{} {}'.format(self.manufacturer.name, self.model)
|
||||
|
||||
@property
|
||||
@@ -1316,7 +1353,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
name = NullableCharField(
|
||||
name = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -1327,7 +1364,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
blank=True,
|
||||
verbose_name='Serial number'
|
||||
)
|
||||
asset_tag = NullableCharField(
|
||||
asset_tag = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -1437,7 +1474,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super(Device, self).__str__()
|
||||
return self.display_name or super().__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
@@ -1552,7 +1589,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
|
||||
is_new = not bool(self.pk)
|
||||
|
||||
super(Device, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# If this is a new Device, instantiate all of the related components per the DeviceType definition
|
||||
if is_new:
|
||||
@@ -2055,7 +2092,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
if self.pk and self.mode is not IFACE_MODE_TAGGED:
|
||||
self.tagged_vlans.clear()
|
||||
|
||||
return super(Interface, self).save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def log_change(self, user, request_id, action):
|
||||
"""
|
||||
@@ -2351,7 +2388,7 @@ class InventoryItem(ComponentModel):
|
||||
verbose_name='Serial number',
|
||||
blank=True
|
||||
)
|
||||
asset_tag = NullableCharField(
|
||||
asset_tag = models.CharField(
|
||||
max_length=50,
|
||||
unique=True,
|
||||
blank=True,
|
||||
@@ -2386,7 +2423,7 @@ class InventoryItem(ComponentModel):
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.name or '{' + self.device.pk + '}',
|
||||
self.device.name or '{{{}}}'.format(self.device.pk),
|
||||
self.name,
|
||||
self.manufacturer.name if self.manufacturer else None,
|
||||
self.part_id,
|
||||
@@ -2495,10 +2532,10 @@ class Cable(ChangeLoggedModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
length_unit = models.CharField(
|
||||
length_unit = models.PositiveSmallIntegerField(
|
||||
choices=CABLE_LENGTH_UNIT_CHOICES,
|
||||
max_length=2,
|
||||
blank=True
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
# Stores the normalized length (in meters) for database ordering
|
||||
_abs_length = models.DecimalField(
|
||||
@@ -2520,74 +2557,76 @@ class Cable(ChangeLoggedModel):
|
||||
('termination_b_type', 'termination_b_id'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(Cable, self).__init__(*args, **kwargs)
|
||||
|
||||
# Create an ID string for use by __str__(). We have to save a copy of pk since it's nullified after .delete()
|
||||
# is called.
|
||||
self.id_string = '#{}'.format(self.pk)
|
||||
|
||||
def __str__(self):
|
||||
return self.label or self.id_string
|
||||
if self.label:
|
||||
return self.label
|
||||
|
||||
# Save a copy of the PK on the instance since it's nullified if .delete() is called
|
||||
if not hasattr(self, 'id_string'):
|
||||
self.id_string = '#{}'.format(self.pk)
|
||||
|
||||
return self.id_string
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:cable', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Check that termination types are compatible
|
||||
type_a = self.termination_a_type.model
|
||||
type_b = self.termination_b_type.model
|
||||
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
|
||||
raise ValidationError("Incompatible termination types: {} and {}".format(
|
||||
self.termination_a_type, self.termination_b_type
|
||||
))
|
||||
if self.termination_a and self.termination_b:
|
||||
|
||||
# A termination point cannot be connected to itself
|
||||
if self.termination_a == self.termination_b:
|
||||
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
|
||||
type_a = self.termination_a_type.model
|
||||
type_b = self.termination_b_type.model
|
||||
|
||||
# A front port cannot be connected to its corresponding rear port
|
||||
if (
|
||||
type_a in ['frontport', 'rearport'] and
|
||||
type_b in ['frontport', 'rearport'] and
|
||||
(
|
||||
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
|
||||
getattr(self.termination_b, 'rear_port', None) == self.termination_a
|
||||
)
|
||||
):
|
||||
raise ValidationError("A front port cannot be connected to it corresponding rear port")
|
||||
# Check that termination types are compatible
|
||||
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
|
||||
raise ValidationError("Incompatible termination types: {} and {}".format(
|
||||
self.termination_a_type, self.termination_b_type
|
||||
))
|
||||
|
||||
# Check for an existing Cable connected to either termination object
|
||||
if self.termination_a.cable not in (None, self):
|
||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
||||
self.termination_a, self.termination_a.cable_id
|
||||
))
|
||||
if self.termination_b.cable not in (None, self):
|
||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
||||
self.termination_b, self.termination_b.cable_id
|
||||
))
|
||||
# A termination point cannot be connected to itself
|
||||
if self.termination_a == self.termination_b:
|
||||
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
|
||||
|
||||
# Virtual interfaces cannot be connected
|
||||
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
|
||||
if (
|
||||
(
|
||||
isinstance(endpoint_a, Interface) and
|
||||
endpoint_a.form_factor == IFACE_FF_VIRTUAL
|
||||
) or
|
||||
(
|
||||
isinstance(endpoint_b, Interface) and
|
||||
endpoint_b.form_factor == IFACE_FF_VIRTUAL
|
||||
)
|
||||
):
|
||||
raise ValidationError("Cannot connect to a virtual interface")
|
||||
# A front port cannot be connected to its corresponding rear port
|
||||
if (
|
||||
type_a in ['frontport', 'rearport'] and
|
||||
type_b in ['frontport', 'rearport'] and
|
||||
(
|
||||
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
|
||||
getattr(self.termination_b, 'rear_port', None) == self.termination_a
|
||||
)
|
||||
):
|
||||
raise ValidationError("A front port cannot be connected to it corresponding rear port")
|
||||
|
||||
# Check for an existing Cable connected to either termination object
|
||||
if self.termination_a.cable not in (None, self):
|
||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
||||
self.termination_a, self.termination_a.cable_id
|
||||
))
|
||||
if self.termination_b.cable not in (None, self):
|
||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
||||
self.termination_b, self.termination_b.cable_id
|
||||
))
|
||||
|
||||
# Virtual interfaces cannot be connected
|
||||
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
|
||||
if (
|
||||
(
|
||||
isinstance(endpoint_a, Interface) and
|
||||
endpoint_a.form_factor == IFACE_FF_VIRTUAL
|
||||
) or
|
||||
(
|
||||
isinstance(endpoint_b, Interface) and
|
||||
endpoint_b.form_factor == IFACE_FF_VIRTUAL
|
||||
)
|
||||
):
|
||||
raise ValidationError("Cannot connect to a virtual interface")
|
||||
|
||||
# Validate length and length_unit
|
||||
if self.length and not self.length_unit:
|
||||
if self.length is not None and self.length_unit is None:
|
||||
raise ValidationError("Must specify a unit when setting a cable length")
|
||||
if self.length_unit and self.length is None:
|
||||
self.length_unit = ''
|
||||
elif self.length is None:
|
||||
self.length_unit = None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -2595,7 +2634,7 @@ class Cable(ChangeLoggedModel):
|
||||
if self.length and self.length_unit:
|
||||
self._abs_length = to_meters(self.length, self.length_unit)
|
||||
|
||||
super(Cable, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@@ -2611,6 +2650,9 @@ class Cable(ChangeLoggedModel):
|
||||
self.length_unit,
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return 'success' if self.status else 'info'
|
||||
|
||||
def get_path_endpoints(self):
|
||||
"""
|
||||
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
|
||||
@@ -2629,66 +2671,7 @@ class Cable(ChangeLoggedModel):
|
||||
path_status = CONNECTION_STATUS_PLANNED
|
||||
break
|
||||
|
||||
# (A path end, B path end, connected/planned)
|
||||
return a_path[-1][2], b_path[-1][2], path_status
|
||||
a_endpoint = a_path[-1][2]
|
||||
b_endpoint = b_path[-1][2]
|
||||
|
||||
|
||||
#
|
||||
# Connection proxy models
|
||||
#
|
||||
|
||||
class ConsoleConnection(ConsolePort):
|
||||
|
||||
csv_headers = [
|
||||
'console_server', 'port', 'device', 'console_port', 'connection_status',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
|
||||
self.connected_endpoint.name if self.connected_endpoint else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
)
|
||||
|
||||
|
||||
class PowerConnection(PowerPort):
|
||||
|
||||
csv_headers = [
|
||||
'pdu', 'outlet', 'device', 'power_port', 'connection_status',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
|
||||
self.connected_endpoint.name if self.connected_endpoint else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
)
|
||||
|
||||
|
||||
class InterfaceConnection(Interface):
|
||||
|
||||
csv_headers = [
|
||||
'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.connected_endpoint.device.identifier if self.connected_endpoint else None,
|
||||
self.connected_endpoint.name if self.connected_endpoint else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
)
|
||||
return a_endpoint, b_endpoint, path_status
|
||||
|
||||
@@ -62,7 +62,7 @@ def nullify_connected_endpoints(instance, **kwargs):
|
||||
instance.termination_b.save()
|
||||
|
||||
# If this Cable was part of a complete path, tear it down
|
||||
if endpoint_a is not None and endpoint_b is not None:
|
||||
if hasattr(endpoint_a, 'connected_endpoint') and hasattr(endpoint_b, 'connected_endpoint'):
|
||||
endpoint_a.connected_endpoint = None
|
||||
endpoint_a.connection_status = None
|
||||
endpoint_a.save()
|
||||
|
||||
@@ -4,11 +4,10 @@ from django_tables2.utils import Accessor
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
||||
from .models import (
|
||||
Cable, ConsoleConnection, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerConnection, PowerOutlet, PowerOutletTemplate,
|
||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
|
||||
REGION_LINK = """
|
||||
@@ -30,7 +29,8 @@ SITE_REGION_LINK = """
|
||||
"""
|
||||
|
||||
COLOR_LABEL = """
|
||||
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
|
||||
{% load helpers %}
|
||||
<label class="label" style="color: {{ record.color|fgcolor }}; background-color: #{{ record.color }}">{{ record }}</label>
|
||||
"""
|
||||
|
||||
DEVICE_LINK = """
|
||||
@@ -136,7 +136,8 @@ PLATFORM_ACTIONS = """
|
||||
"""
|
||||
|
||||
DEVICE_ROLE = """
|
||||
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
|
||||
{% load helpers %}
|
||||
<label class="label" style="color: {{ record.device_role.color|fgcolor }}; background-color: #{{ record.device_role.color }}">{{ value }}</label>
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
@@ -180,7 +181,7 @@ CABLE_TERMINATION_PARENT = """
|
||||
"""
|
||||
|
||||
CABLE_LENGTH = """
|
||||
{% if record.length %}{{ record.length }}{{ record.length_unit }}{% else %}—{% endif %}
|
||||
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
@@ -195,7 +196,7 @@ class RegionTable(BaseTable):
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=REGION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
@@ -238,7 +239,7 @@ class RackGroupTable(BaseTable):
|
||||
slug = tables.Column()
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKGROUP_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
@@ -257,7 +258,7 @@ class RackRoleTable(BaseTable):
|
||||
rack_count = tables.Column(verbose_name='Racks')
|
||||
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'}},
|
||||
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -308,7 +309,7 @@ class RackReservationTable(BaseTable):
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -326,7 +327,7 @@ class ManufacturerTable(BaseTable):
|
||||
devicetype_count = tables.Column(verbose_name='Device Types')
|
||||
platform_count = tables.Column(verbose_name='Platforms')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -462,7 +463,7 @@ class DeviceRoleTable(BaseTable):
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
@@ -491,7 +492,7 @@ class PlatformTable(BaseTable):
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=PLATFORM_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
@@ -517,7 +518,7 @@ class DeviceTable(BaseTable):
|
||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||
device_type = tables.LinkColumn(
|
||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name
|
||||
text=lambda record: record.device_type.display_name
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -646,6 +647,9 @@ class CableTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name=''
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
length = tables.TemplateColumn(
|
||||
template_code=CABLE_LENGTH,
|
||||
order_by='_abs_length'
|
||||
@@ -683,7 +687,7 @@ class ConsoleConnectionTable(BaseTable):
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleConnection
|
||||
model = ConsolePort
|
||||
fields = ('console_server', 'connected_endpoint', 'device', 'name', 'connection_status')
|
||||
|
||||
|
||||
@@ -706,7 +710,7 @@ class PowerConnectionTable(BaseTable):
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerConnection
|
||||
model = PowerPort
|
||||
fields = ('pdu', 'connected_endpoint', 'device', 'name', 'connection_status')
|
||||
|
||||
|
||||
@@ -745,7 +749,7 @@ class InterfaceConnectionTable(BaseTable):
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InterfaceConnection
|
||||
model = Interface
|
||||
fields = (
|
||||
'device_a', 'interface_a', 'description_a', 'device_b', 'interface_b', 'description_b', 'connection_status',
|
||||
)
|
||||
@@ -775,7 +779,7 @@ class VirtualChassisTable(BaseTable):
|
||||
member_count = tables.Column(verbose_name='Members')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=VIRTUALCHASSIS_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class RegionTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(RegionTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.region1 = Region.objects.create(name='Test Region 1', slug='test-region-1')
|
||||
self.region2 = Region.objects.create(name='Test Region 2', slug='test-region-2')
|
||||
@@ -121,7 +121,7 @@ class SiteTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(SiteTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.region1 = Region.objects.create(name='Test Region 1', slug='test-region-1')
|
||||
self.region2 = Region.objects.create(name='Test Region 2', slug='test-region-2')
|
||||
@@ -256,7 +256,7 @@ class RackGroupTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(RackGroupTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
@@ -366,7 +366,7 @@ class RackRoleTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(RackRoleTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000')
|
||||
self.rackrole2 = RackRole.objects.create(name='Test Rack Role 2', slug='test-rack-role-2', color='00ff00')
|
||||
@@ -474,7 +474,7 @@ class RackTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(RackTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
@@ -608,7 +608,7 @@ class RackReservationTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(RackReservationTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.rack1 = Rack.objects.create(site=self.site1, name='Test Rack 1')
|
||||
@@ -719,7 +719,7 @@ class ManufacturerTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ManufacturerTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.manufacturer1 = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.manufacturer2 = Manufacturer.objects.create(name='Test Manufacturer 2', slug='test-manufacturer-2')
|
||||
@@ -820,7 +820,7 @@ class DeviceTypeTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(DeviceTypeTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.manufacturer1 = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.manufacturer2 = Manufacturer.objects.create(name='Test Manufacturer 2', slug='test-manufacturer-2')
|
||||
@@ -855,7 +855,7 @@ class DeviceTypeTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'manufacturer', 'model', 'slug', 'url']
|
||||
['display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_devicetype(self):
|
||||
@@ -936,7 +936,7 @@ class ConsolePortTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ConsolePortTemplateTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1036,7 +1036,7 @@ class ConsoleServerPortTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ConsoleServerPortTemplateTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1136,7 +1136,7 @@ class PowerPortTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(PowerPortTemplateTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1236,7 +1236,7 @@ class PowerOutletTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(PowerOutletTemplateTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1336,7 +1336,7 @@ class InterfaceTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(InterfaceTemplateTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1436,7 +1436,7 @@ class DeviceBayTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(DeviceBayTemplateTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
self.devicetype = DeviceType.objects.create(
|
||||
@@ -1536,7 +1536,7 @@ class DeviceRoleTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(DeviceRoleTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.devicerole1 = DeviceRole.objects.create(
|
||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||
@@ -1650,7 +1650,7 @@ class PlatformTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(PlatformTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.platform1 = Platform.objects.create(name='Test Platform 1', slug='test-platform-1')
|
||||
self.platform2 = Platform.objects.create(name='Test Platform 2', slug='test-platform-2')
|
||||
@@ -1751,7 +1751,7 @@ class DeviceTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(DeviceTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
@@ -1913,7 +1913,7 @@ class ConsolePortTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ConsolePortTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -1951,7 +1951,7 @@ class ConsolePortTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['cable', 'device', 'id', 'name', 'url']
|
||||
['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_consoleport(self):
|
||||
@@ -2026,7 +2026,7 @@ class ConsoleServerPortTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ConsoleServerPortTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2064,7 +2064,7 @@ class ConsoleServerPortTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['cable', 'device', 'id', 'name', 'url']
|
||||
['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_consoleserverport(self):
|
||||
@@ -2137,7 +2137,7 @@ class PowerPortTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(PowerPortTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2175,7 +2175,7 @@ class PowerPortTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['cable', 'device', 'id', 'name', 'url']
|
||||
['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_powerport(self):
|
||||
@@ -2250,7 +2250,7 @@ class PowerOutletTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(PowerOutletTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2288,7 +2288,7 @@ class PowerOutletTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['cable', 'device', 'id', 'name', 'url']
|
||||
['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_poweroutlet(self):
|
||||
@@ -2361,7 +2361,7 @@ class InterfaceTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(InterfaceTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2425,7 +2425,7 @@ class InterfaceTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['cable', 'device', 'id', 'name', 'url']
|
||||
['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_interface(self):
|
||||
@@ -2560,7 +2560,7 @@ class DeviceBayTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(DeviceBayTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2683,7 +2683,7 @@ class InventoryItemTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(InventoryItemTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2799,7 +2799,7 @@ class CableTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(CableTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -2940,7 +2940,7 @@ class ConnectionTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ConnectionTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.site = Site.objects.create(
|
||||
name='Test Site 1', slug='test-site-1'
|
||||
@@ -3304,7 +3304,7 @@ class ConnectedDeviceTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ConnectedDeviceTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||
@@ -3346,7 +3346,7 @@ class VirtualChassisTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(VirtualChassisTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site', slug='test-site')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer', slug='test-manufacturer')
|
||||
|
||||
458
netbox/dcim/tests/test_views.py
Normal file
458
netbox/dcim/tests/test_views.py
Normal file
@@ -0,0 +1,458 @@
|
||||
import urllib.parse
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.constants import CABLE_TYPE_CAT6, IFACE_FF_1GE_FIXED
|
||||
from dcim.models import (
|
||||
Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
|
||||
RackReservation, RackRole, Site, Region, VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
class RegionTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
# Create three Regions
|
||||
for i in range(1, 4):
|
||||
Region(name='Region {}'.format(i), slug='region-{}'.format(i)).save()
|
||||
|
||||
def test_region_list(self):
|
||||
|
||||
url = reverse('dcim:region_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class SiteTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
region = Region(name='Region 1', slug='region-1')
|
||||
region.save()
|
||||
|
||||
Site.objects.bulk_create([
|
||||
Site(name='Site 1', slug='site-1', region=region),
|
||||
Site(name='Site 2', slug='site-2', region=region),
|
||||
Site(name='Site 3', slug='site-3', region=region),
|
||||
])
|
||||
|
||||
def test_site_list(self):
|
||||
|
||||
url = reverse('dcim:site_list')
|
||||
params = {
|
||||
"region": Region.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_site(self):
|
||||
|
||||
site = Site.objects.first()
|
||||
response = self.client.get(site.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RackGroupTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
RackGroup.objects.bulk_create([
|
||||
RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
|
||||
RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
|
||||
RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
|
||||
])
|
||||
|
||||
def test_rackgroup_list(self):
|
||||
|
||||
url = reverse('dcim:rackgroup_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RackTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
RackRole.objects.bulk_create([
|
||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||
RackRole(name='Rack Role 2', slug='rack-role-2'),
|
||||
RackRole(name='Rack Role 3', slug='rack-role-3'),
|
||||
])
|
||||
|
||||
def test_rackrole_list(self):
|
||||
|
||||
url = reverse('dcim:rackrole_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RackReservationTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
User = get_user_model()
|
||||
user = User(username='testuser', email='testuser@example.com')
|
||||
user.save()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
rack = Rack(name='Rack 1', site=site)
|
||||
rack.save()
|
||||
|
||||
RackReservation.objects.bulk_create([
|
||||
RackReservation(rack=rack, user=user, units=[1, 2, 3], description='Reservation 1'),
|
||||
RackReservation(rack=rack, user=user, units=[4, 5, 6], description='Reservation 2'),
|
||||
RackReservation(rack=rack, user=user, units=[7, 8, 9], description='Reservation 3'),
|
||||
])
|
||||
|
||||
def test_rackreservation_list(self):
|
||||
|
||||
url = reverse('dcim:rackreservation_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RackTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
Rack.objects.bulk_create([
|
||||
Rack(name='Rack 1', site=site),
|
||||
Rack(name='Rack 2', site=site),
|
||||
Rack(name='Rack 3', site=site),
|
||||
])
|
||||
|
||||
def test_rack_list(self):
|
||||
|
||||
url = reverse('dcim:rack_list')
|
||||
params = {
|
||||
"site": Site.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_rack(self):
|
||||
|
||||
rack = Rack.objects.first()
|
||||
response = self.client.get(rack.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ManufacturerTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Manufacturer.objects.bulk_create([
|
||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
||||
])
|
||||
|
||||
def test_manufacturer_list(self):
|
||||
|
||||
url = reverse('dcim:manufacturer_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class DeviceTypeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
DeviceType.objects.bulk_create([
|
||||
DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer),
|
||||
DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer),
|
||||
DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturer),
|
||||
])
|
||||
|
||||
def test_devicetype_list(self):
|
||||
|
||||
url = reverse('dcim:devicetype_list')
|
||||
params = {
|
||||
"manufacturer": Manufacturer.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_devicetype(self):
|
||||
|
||||
devicetype = DeviceType.objects.first()
|
||||
response = self.client.get(devicetype.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class DeviceRoleTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
DeviceRole.objects.bulk_create([
|
||||
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||
])
|
||||
|
||||
def test_devicerole_list(self):
|
||||
|
||||
url = reverse('dcim:devicerole_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class PlatformTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Platform.objects.bulk_create([
|
||||
Platform(name='Platform 1', slug='platform-1'),
|
||||
Platform(name='Platform 2', slug='platform-2'),
|
||||
Platform(name='Platform 3', slug='platform-3'),
|
||||
])
|
||||
|
||||
def test_platform_list(self):
|
||||
|
||||
url = reverse('dcim:platform_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class DeviceTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
|
||||
devicetype.save()
|
||||
|
||||
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
|
||||
devicerole.save()
|
||||
|
||||
Device.objects.bulk_create([
|
||||
Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
|
||||
Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole),
|
||||
Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
|
||||
])
|
||||
|
||||
def test_device_list(self):
|
||||
|
||||
url = reverse('dcim:device_list')
|
||||
params = {
|
||||
"device_type_id": DeviceType.objects.first().pk,
|
||||
"role": DeviceRole.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_device(self):
|
||||
|
||||
device = Device.objects.first()
|
||||
response = self.client.get(device.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class InventoryItemTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
|
||||
devicetype.save()
|
||||
|
||||
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
|
||||
devicerole.save()
|
||||
|
||||
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
|
||||
device.save()
|
||||
|
||||
InventoryItem.objects.bulk_create([
|
||||
InventoryItem(device=device, name='Inventory Item 1'),
|
||||
InventoryItem(device=device, name='Inventory Item 2'),
|
||||
InventoryItem(device=device, name='Inventory Item 3'),
|
||||
])
|
||||
|
||||
def test_inventoryitem_list(self):
|
||||
|
||||
url = reverse('dcim:inventoryitem_list')
|
||||
params = {
|
||||
"device_id": Device.objects.first().pk,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_inventoryitem(self):
|
||||
|
||||
inventoryitem = InventoryItem.objects.first()
|
||||
response = self.client.get(inventoryitem.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
|
||||
devicetype.save()
|
||||
|
||||
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
|
||||
devicerole.save()
|
||||
|
||||
device1 = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
|
||||
device1.save()
|
||||
device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole)
|
||||
device2.save()
|
||||
|
||||
iface1 = Interface(device=device1, name='Interface 1', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface1.save()
|
||||
iface2 = Interface(device=device1, name='Interface 2', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface2.save()
|
||||
iface3 = Interface(device=device1, name='Interface 3', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface3.save()
|
||||
iface4 = Interface(device=device2, name='Interface 1', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface4.save()
|
||||
iface5 = Interface(device=device2, name='Interface 2', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface5.save()
|
||||
iface6 = Interface(device=device2, name='Interface 3', form_factor=IFACE_FF_1GE_FIXED)
|
||||
iface6.save()
|
||||
|
||||
Cable(termination_a=iface1, termination_b=iface4, type=CABLE_TYPE_CAT6).save()
|
||||
Cable(termination_a=iface2, termination_b=iface5, type=CABLE_TYPE_CAT6).save()
|
||||
Cable(termination_a=iface3, termination_b=iface6, type=CABLE_TYPE_CAT6).save()
|
||||
|
||||
def test_cable_list(self):
|
||||
|
||||
url = reverse('dcim:cable_list')
|
||||
params = {
|
||||
"type": CABLE_TYPE_CAT6,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_cable(self):
|
||||
|
||||
cable = Cable.objects.first()
|
||||
response = self.client.get(cable.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class VirtualMachineTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||
)
|
||||
device_role = DeviceRole.objects.create(
|
||||
name='Device Role', slug='device-role-1'
|
||||
)
|
||||
|
||||
# Create 9 member Devices
|
||||
device1 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 1', site=site
|
||||
)
|
||||
device2 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 2', site=site
|
||||
)
|
||||
device3 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 3', site=site
|
||||
)
|
||||
device4 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 4', site=site
|
||||
)
|
||||
device5 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 5', site=site
|
||||
)
|
||||
device6 = Device.objects.create(
|
||||
device_type=device_type, device_role=device_role, name='Device 6', site=site
|
||||
)
|
||||
|
||||
# Create three VirtualChassis with two members each
|
||||
vc1 = VirtualChassis.objects.create(master=device1, domain='test-domain-1')
|
||||
Device.objects.filter(pk=device2.pk).update(virtual_chassis=vc1, vc_position=2)
|
||||
vc2 = VirtualChassis.objects.create(master=device3, domain='test-domain-2')
|
||||
Device.objects.filter(pk=device4.pk).update(virtual_chassis=vc2, vc_position=2)
|
||||
vc3 = VirtualChassis.objects.create(master=device5, domain='test-domain-3')
|
||||
Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2)
|
||||
|
||||
def test_virtualchassis_list(self):
|
||||
|
||||
url = reverse('dcim:virtualchassis_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_virtualchassis(self):
|
||||
|
||||
virtualchassis = VirtualChassis.objects.first()
|
||||
response = self.client.get(virtualchassis.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -215,6 +215,7 @@ urlpatterns = [
|
||||
# Front ports
|
||||
# url(r'^devices/front-ports/add/$', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||
url(r'^devices/(?P<pk>\d+)/front-ports/add/$', views.FrontPortCreateView.as_view(), name='frontport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/front-ports/edit/$', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/front-ports/delete/$', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
|
||||
url(r'^front-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
url(r'^front-ports/(?P<pk>\d+)/edit/$', views.FrontPortEditView.as_view(), name='frontport_edit'),
|
||||
@@ -226,6 +227,7 @@ urlpatterns = [
|
||||
# Rear ports
|
||||
# url(r'^devices/rear-ports/add/$', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
url(r'^devices/(?P<pk>\d+)/rear-ports/add/$', views.RearPortCreateView.as_view(), name='rearport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/rear-ports/edit/$', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/rear-ports/delete/$', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
|
||||
url(r'^rear-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
url(r'^rear-ports/(?P<pk>\d+)/edit/$', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
@@ -17,6 +20,7 @@ from ipam.models import Prefix, VLAN
|
||||
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.utils import csv_format
|
||||
from utilities.views import (
|
||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
|
||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
@@ -24,11 +28,10 @@ from utilities.views import (
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
from .models import (
|
||||
Cable, ConsoleConnection, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerConnection, PowerOutlet, PowerOutletTemplate,
|
||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
@@ -50,7 +53,16 @@ class BulkRenameView(GetReturnURLMixin, View):
|
||||
|
||||
if form.is_valid():
|
||||
for obj in selected_objects:
|
||||
obj.new_name = obj.name.replace(form.cleaned_data['find'], form.cleaned_data['replace'])
|
||||
find = form.cleaned_data['find']
|
||||
replace = form.cleaned_data['replace']
|
||||
if form.cleaned_data['use_regex']:
|
||||
try:
|
||||
obj.new_name = re.sub(find, replace, obj.name)
|
||||
# Catch regex group reference errors
|
||||
except re.error:
|
||||
obj.new_name = obj.name
|
||||
else:
|
||||
obj.new_name = obj.name.replace(find, replace)
|
||||
|
||||
if '_apply' in request.POST:
|
||||
for obj in selected_objects:
|
||||
@@ -124,7 +136,13 @@ class BulkDisconnectView(GetReturnURLMixin, View):
|
||||
#
|
||||
|
||||
class RegionListView(ObjectListView):
|
||||
queryset = Region.objects.annotate(site_count=Count('sites'))
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filter = filters.RegionFilter
|
||||
filter_form = forms.RegionFilterForm
|
||||
table = tables.RegionTable
|
||||
@@ -151,7 +169,7 @@ class RegionBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_region'
|
||||
queryset = Region.objects.annotate(site_count=Count('sites'))
|
||||
queryset = Region.objects.all()
|
||||
filter = filters.RegionFilter
|
||||
table = tables.RegionTable
|
||||
default_return_url = 'dcim:region_list'
|
||||
@@ -336,8 +354,9 @@ class RackElevationListView(View):
|
||||
total_count = racks.count()
|
||||
|
||||
# Pagination
|
||||
paginator = EnhancedPaginator(racks, 25)
|
||||
per_page = request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
page_number = request.GET.get('page', 1)
|
||||
paginator = EnhancedPaginator(racks, per_page)
|
||||
try:
|
||||
page = paginator.page(page_number)
|
||||
except PageNotAnInteger:
|
||||
@@ -1349,6 +1368,14 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = FrontPort
|
||||
|
||||
|
||||
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.FrontPortTable
|
||||
form = forms.FrontPortBulkEditForm
|
||||
|
||||
|
||||
class FrontPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
@@ -1393,6 +1420,14 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = RearPort
|
||||
|
||||
|
||||
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.RearPortTable
|
||||
form = forms.RearPortBulkEditForm
|
||||
|
||||
|
||||
class RearPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
@@ -1530,6 +1565,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = ConsolePort
|
||||
model_form = forms.ConsolePortForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -1541,6 +1577,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = ConsoleServerPort
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -1552,6 +1589,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = PowerPort
|
||||
model_form = forms.PowerPortForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -1563,6 +1601,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = PowerOutlet
|
||||
model_form = forms.PowerOutletForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -1574,6 +1613,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
|
||||
form = forms.DeviceBulkAddInterfaceForm
|
||||
model = Interface
|
||||
model_form = forms.InterfaceForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -1585,6 +1625,7 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = DeviceBay
|
||||
model_form = forms.DeviceBayForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -1688,7 +1729,7 @@ class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class ConsoleConnectionsListView(ObjectListView):
|
||||
queryset = ConsoleConnection.objects.select_related(
|
||||
queryset = ConsolePort.objects.select_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
@@ -1700,9 +1741,25 @@ class ConsoleConnectionsListView(ObjectListView):
|
||||
table = tables.ConsoleConnectionTable
|
||||
template_name = 'dcim/console_connections_list.html'
|
||||
|
||||
def queryset_to_csv(self):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.join(['console_server', 'port', 'device', 'console_port', 'connection_status'])
|
||||
]
|
||||
for obj in self.queryset:
|
||||
csv = csv_format([
|
||||
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
|
||||
obj.connected_endpoint.name if obj.connected_endpoint else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj.get_connection_status_display(),
|
||||
])
|
||||
csv_data.append(csv)
|
||||
return csv_data
|
||||
|
||||
|
||||
class PowerConnectionsListView(ObjectListView):
|
||||
queryset = PowerConnection.objects.select_related(
|
||||
queryset = PowerPort.objects.select_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
@@ -1714,9 +1771,25 @@ class PowerConnectionsListView(ObjectListView):
|
||||
table = tables.PowerConnectionTable
|
||||
template_name = 'dcim/power_connections_list.html'
|
||||
|
||||
def queryset_to_csv(self):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.join(['pdu', 'outlet', 'device', 'power_port', 'connection_status'])
|
||||
]
|
||||
for obj in self.queryset:
|
||||
csv = csv_format([
|
||||
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
|
||||
obj.connected_endpoint.name if obj.connected_endpoint else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj.get_connection_status_display(),
|
||||
])
|
||||
csv_data.append(csv)
|
||||
return csv_data
|
||||
|
||||
|
||||
class InterfaceConnectionsListView(ObjectListView):
|
||||
queryset = InterfaceConnection.objects.select_related(
|
||||
queryset = Interface.objects.select_related(
|
||||
'device', 'cable', '_connected_interface__device'
|
||||
).filter(
|
||||
# Avoid duplicate connections by only selecting the lower PK in a connected pair
|
||||
@@ -1730,6 +1803,28 @@ class InterfaceConnectionsListView(ObjectListView):
|
||||
table = tables.InterfaceConnectionTable
|
||||
template_name = 'dcim/interface_connections_list.html'
|
||||
|
||||
def queryset_to_csv(self):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.join([
|
||||
'device_a', 'interface_a', 'interface_a_description',
|
||||
'device_b', 'interface_b', 'interface_b_description',
|
||||
'connection_status'
|
||||
])
|
||||
]
|
||||
for obj in self.queryset:
|
||||
csv = csv_format([
|
||||
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
|
||||
obj.connected_endpoint.name if obj.connected_endpoint else None,
|
||||
obj.connected_endpoint.description if obj.connected_endpoint else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj.description,
|
||||
obj.get_connection_status_display(),
|
||||
])
|
||||
csv_data.append(csv)
|
||||
return csv_data
|
||||
|
||||
|
||||
#
|
||||
# Inventory items
|
||||
|
||||
@@ -28,9 +28,10 @@ class WebhookForm(forms.ModelForm):
|
||||
exclude = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WebhookForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
order_content_types(self.fields['obj_type'])
|
||||
if 'obj_type' in self.fields:
|
||||
order_content_types(self.fields['obj_type'])
|
||||
|
||||
|
||||
@admin.register(Webhook, site=admin_site)
|
||||
@@ -56,7 +57,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
exclude = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
order_content_types(self.fields['obj_type'])
|
||||
|
||||
@@ -96,7 +97,7 @@ class ExportTemplateForm(forms.ModelForm):
|
||||
exclude = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ExportTemplateForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Format ContentType choices
|
||||
order_content_types(self.fields['content_type'])
|
||||
|
||||
@@ -105,7 +105,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
custom_fields[cfv.field.name] = cfv.value
|
||||
instance.custom_fields = custom_fields
|
||||
|
||||
super(CustomFieldModelSerializer, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.instance is not None:
|
||||
|
||||
@@ -137,7 +137,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
instance = super(CustomFieldModelSerializer, self).create(validated_data)
|
||||
instance = super().create(validated_data)
|
||||
|
||||
# Save custom fields
|
||||
if custom_fields is not None:
|
||||
@@ -152,7 +152,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
instance = super(CustomFieldModelSerializer, self).update(instance, validated_data)
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
# Save custom fields
|
||||
if custom_fields is not None:
|
||||
|
||||
@@ -108,7 +108,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
)
|
||||
|
||||
# Enforce model validation
|
||||
super(ImageAttachmentSerializer, self).validate(data)
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class CustomFieldModelViewSet(ModelViewSet):
|
||||
custom_field_choices[cfc.id] = cfc.value
|
||||
custom_field_choices = custom_field_choices
|
||||
|
||||
context = super(CustomFieldModelViewSet, self).get_serializer_context()
|
||||
context = super().get_serializer_context()
|
||||
context.update({
|
||||
'custom_fields': custom_fields,
|
||||
'custom_field_choices': custom_field_choices,
|
||||
@@ -59,7 +59,7 @@ class CustomFieldModelViewSet(ModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
# Prefetch custom field values
|
||||
return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field')
|
||||
return super().get_queryset().prefetch_related('custom_field_values__field')
|
||||
|
||||
|
||||
#
|
||||
@@ -99,10 +99,9 @@ class TopologyMapViewSet(ModelViewSet):
|
||||
|
||||
try:
|
||||
data = tmap.render(img_format=img_format)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
return HttpResponse(
|
||||
"There was an error generating the requested graph. Ensure that the GraphViz executables have been "
|
||||
"installed correctly."
|
||||
"There was an error generating the requested graph: %s" % e
|
||||
)
|
||||
|
||||
response = HttpResponse(data, content_type='image/{}'.format(img_format))
|
||||
|
||||
@@ -17,7 +17,7 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
def __init__(self, custom_field, *args, **kwargs):
|
||||
self.cf_type = custom_field.type
|
||||
self.filter_logic = custom_field.filter_logic
|
||||
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, queryset, value):
|
||||
|
||||
@@ -31,12 +31,12 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
# Treat 0 as None
|
||||
if int(value) == 0:
|
||||
return queryset.exclude(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__field__name=self.field_name,
|
||||
)
|
||||
# Match on exact CustomFieldChoice PK
|
||||
else:
|
||||
return queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__field__name=self.field_name,
|
||||
custom_field_values__serialized_value=value,
|
||||
)
|
||||
except ValueError:
|
||||
@@ -45,12 +45,12 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
# Apply the assigned filter logic (exact or loose)
|
||||
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
|
||||
queryset = queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__field__name=self.field_name,
|
||||
custom_field_values__serialized_value=value
|
||||
)
|
||||
else:
|
||||
queryset = queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__field__name=self.field_name,
|
||||
custom_field_values__serialized_value__icontains=value
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
|
||||
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=CF_FILTER_DISABLED)
|
||||
|
||||
@@ -11,8 +11,8 @@ from taggit.models import Tag
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, FilterChoiceField,
|
||||
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
|
||||
FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
||||
)
|
||||
from .constants import (
|
||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
@@ -102,7 +102,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
self.custom_fields = []
|
||||
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
|
||||
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Add all applicable CustomFields to the form
|
||||
custom_fields = []
|
||||
@@ -138,7 +138,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
cfv.save()
|
||||
|
||||
def save(self, commit=True):
|
||||
obj = super(CustomFieldForm, self).save(commit)
|
||||
obj = super().save(commit)
|
||||
|
||||
# Handle custom fields the same way we do M2M fields
|
||||
if commit:
|
||||
@@ -152,7 +152,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
class CustomFieldBulkEditForm(BulkEditForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomFieldBulkEditForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.custom_fields = []
|
||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||
@@ -175,7 +175,7 @@ class CustomFieldFilterForm(forms.Form):
|
||||
|
||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||
|
||||
super(CustomFieldFilterForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Add all applicable CustomFields to the form
|
||||
custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items()
|
||||
@@ -193,13 +193,15 @@ class TagForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['name', 'slug']
|
||||
fields = [
|
||||
'name', 'slug',
|
||||
]
|
||||
|
||||
|
||||
class AddRemoveTagsForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AddRemoveTagsForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Add add/remove tags fields
|
||||
self.fields['add_tags'] = TagField(required=False)
|
||||
@@ -208,7 +210,10 @@ class AddRemoveTagsForm(forms.Form):
|
||||
|
||||
class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
model = Tag
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -216,10 +221,6 @@ class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
#
|
||||
|
||||
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
regions = TreeNodeMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
)
|
||||
data = JSONField()
|
||||
|
||||
class Meta:
|
||||
@@ -228,6 +229,26 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
|
||||
'tenants', 'data',
|
||||
]
|
||||
widgets = {
|
||||
'regions': APISelectMultiple(
|
||||
api_url="/api/dcim/regions/"
|
||||
),
|
||||
'sites': APISelectMultiple(
|
||||
api_url="/api/dcim/sites/"
|
||||
),
|
||||
'roles': APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/"
|
||||
),
|
||||
'platforms': APISelectMultiple(
|
||||
api_url="/api/dcim/platforms/"
|
||||
),
|
||||
'tenant_groups': APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/"
|
||||
),
|
||||
'tenants': APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
@@ -249,7 +270,9 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
nullable_fields = [
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
@@ -257,29 +280,53 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
region = FilterTreeNodeMultipleChoiceField(
|
||||
region = FilterChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/regions/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
platform = FilterChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/platforms/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tenant_group = FilterChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug'
|
||||
to_field_name='slug',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -291,28 +338,29 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['name', 'image']
|
||||
fields = [
|
||||
'name', 'image',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
model = ObjectChange
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
# TODO: Change time_0 and time_1 to time_after and time_before for django-filter==2.0
|
||||
time_0 = forms.DateTimeField(
|
||||
time_after = forms.DateTimeField(
|
||||
label='After',
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
||||
)
|
||||
)
|
||||
time_1 = forms.DateTimeField(
|
||||
time_before = forms.DateTimeField(
|
||||
label='Before',
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
@@ -327,3 +375,9 @@ class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
queryset=User.objects.order_by('username'),
|
||||
required=False
|
||||
)
|
||||
changed_object_type = forms.ModelChoiceField(
|
||||
queryset=ContentType.objects.order_by('model'),
|
||||
required=False,
|
||||
widget=ContentTypeSelect(),
|
||||
label='Object Type'
|
||||
)
|
||||
|
||||
@@ -29,7 +29,11 @@ def cache_changed_object(instance, **kwargs):
|
||||
|
||||
def _record_object_deleted(request, instance, **kwargs):
|
||||
|
||||
# Record that the object was deleted.
|
||||
# Force resolution of request.user in case it's still a SimpleLazyObject. This seems to happen
|
||||
# occasionally during tests, but haven't been able to determine why.
|
||||
assert request.user.is_authenticated
|
||||
|
||||
# Record that the object was deleted
|
||||
if hasattr(instance, 'log_change'):
|
||||
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:19
|
||||
import re
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
@@ -17,13 +15,14 @@ def verify_postgresql_version(apps, schema_editor):
|
||||
"""
|
||||
Verify that PostgreSQL is version 9.4 or higher.
|
||||
"""
|
||||
# https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQSERVERVERSION
|
||||
DB_MINIMUM_VERSION = 90400 # 9.4.0
|
||||
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT VERSION()")
|
||||
row = cursor.fetchone()
|
||||
pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
|
||||
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
|
||||
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
|
||||
pg_version = connection.pg_version
|
||||
|
||||
if pg_version < DB_MINIMUM_VERSION:
|
||||
raise Exception("PostgreSQL 9.4.0 ({}) or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(DB_MINIMUM_VERSION, pg_version))
|
||||
|
||||
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
|
||||
except OperationalError:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-09-26 21:25
|
||||
from distutils.version import StrictVersion
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
@@ -14,13 +12,14 @@ def verify_postgresql_version(apps, schema_editor):
|
||||
"""
|
||||
Verify that PostgreSQL is version 9.4 or higher.
|
||||
"""
|
||||
# https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQSERVERVERSION
|
||||
DB_MINIMUM_VERSION = 90400 # 9.4.0
|
||||
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT VERSION()")
|
||||
row = cursor.fetchone()
|
||||
pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
|
||||
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
|
||||
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
|
||||
pg_version = connection.pg_version
|
||||
|
||||
if pg_version < DB_MINIMUM_VERSION:
|
||||
raise Exception("PostgreSQL 9.4.0 ({}) or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(DB_MINIMUM_VERSION, pg_version))
|
||||
|
||||
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
|
||||
except OperationalError:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-05 18:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0016_exporttemplate_add_cable'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='mime_type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -14,7 +14,7 @@ from django.template import Template, Context
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||
from utilities.utils import foreground_color
|
||||
from utilities.utils import deepmerge, foreground_color
|
||||
from .constants import *
|
||||
from .querysets import ConfigContextQuerySet
|
||||
|
||||
@@ -261,7 +261,7 @@ class CustomFieldValue(models.Model):
|
||||
if self.pk and self.value is None:
|
||||
self.delete()
|
||||
else:
|
||||
super(CustomFieldValue, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class CustomFieldChoice(models.Model):
|
||||
@@ -293,7 +293,7 @@ class CustomFieldChoice(models.Model):
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
|
||||
pk = self.pk
|
||||
super(CustomFieldChoice, self).delete(using, keep_parents)
|
||||
super().delete(using, keep_parents)
|
||||
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
|
||||
|
||||
|
||||
@@ -357,7 +357,7 @@ class ExportTemplate(models.Model):
|
||||
)
|
||||
template_code = models.TextField()
|
||||
mime_type = models.CharField(
|
||||
max_length=15,
|
||||
max_length=50,
|
||||
blank=True
|
||||
)
|
||||
file_extension = models.CharField(
|
||||
@@ -603,7 +603,7 @@ class ImageAttachment(models.Model):
|
||||
|
||||
_name = self.image.name
|
||||
|
||||
super(ImageAttachment, self).delete(*args, **kwargs)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
# Delete file from disk
|
||||
self.image.delete(save=False)
|
||||
@@ -717,11 +717,11 @@ class ConfigContextModel(models.Model):
|
||||
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
||||
data = OrderedDict()
|
||||
for context in ConfigContext.objects.get_for_object(self):
|
||||
data.update(context.data)
|
||||
data = deepmerge(data, context.data)
|
||||
|
||||
# If the object has local config context data defined, that data overwrites all rendered data
|
||||
# If the object has local config context data defined, merge it last
|
||||
if self.local_context_data is not None:
|
||||
data.update(self.local_context_data)
|
||||
data = deepmerge(data, self.local_context_data)
|
||||
|
||||
return data
|
||||
|
||||
@@ -841,7 +841,7 @@ class ObjectChange(models.Model):
|
||||
self.user_name = self.user.username
|
||||
self.object_repr = str(self.changed_object)
|
||||
|
||||
return super(ObjectChange, self).save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:objectchange', args=[self.pk])
|
||||
|
||||
@@ -68,7 +68,7 @@ class TagTable(BaseTable):
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=TAG_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class GraphTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(GraphTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.graph1 = Graph.objects.create(
|
||||
type=GRAPH_TYPE_SITE, name='Test Graph 1', source='http://example.com/graphs.py?site={{ obj.name }}&foo=1'
|
||||
@@ -118,7 +118,7 @@ class ExportTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ExportTemplateTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.content_type = ContentType.objects.get_for_model(Device)
|
||||
self.exporttemplate1 = ExportTemplate.objects.create(
|
||||
@@ -225,7 +225,7 @@ class TagTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(TagTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
|
||||
self.tag2 = Tag.objects.create(name='Test Tag 2', slug='test-tag-2')
|
||||
@@ -316,7 +316,7 @@ class ConfigContextTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ConfigContextTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.configcontext1 = ConfigContext.objects.create(
|
||||
name='Test Config Context 1',
|
||||
|
||||
@@ -101,7 +101,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(CustomFieldAPITest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Site)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class TaggedItemTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(TaggedItemTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
def test_create_tagged_item(self):
|
||||
|
||||
|
||||
105
netbox/extras/tests/test_views.py
Normal file
105
netbox/extras/tests/test_views.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.models import Site
|
||||
from extras.models import ConfigContext, ObjectChange
|
||||
|
||||
|
||||
class TagTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Tag.objects.bulk_create([
|
||||
Tag(name='Tag 1', slug='tag-1'),
|
||||
Tag(name='Tag 2', slug='tag-2'),
|
||||
Tag(name='Tag 3', slug='tag-3'),
|
||||
])
|
||||
|
||||
def test_tag_list(self):
|
||||
|
||||
url = reverse('extras:tag_list')
|
||||
params = {
|
||||
"q": "tag",
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ConfigContextTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
# Create three ConfigContexts
|
||||
for i in range(1, 4):
|
||||
configcontext = ConfigContext(
|
||||
name='Config Context {}'.format(i),
|
||||
data='{{"foo": {}}}'.format(i)
|
||||
)
|
||||
configcontext.save()
|
||||
configcontext.sites.add(site)
|
||||
|
||||
def test_configcontext_list(self):
|
||||
|
||||
url = reverse('extras:configcontext_list')
|
||||
params = {
|
||||
"q": "foo",
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_configcontext(self):
|
||||
|
||||
configcontext = ConfigContext.objects.first()
|
||||
response = self.client.get(configcontext.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ObjectChangeTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
user = User(username='testuser', email='testuser@example.com')
|
||||
user.save()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
# Create three ObjectChanges
|
||||
for i in range(1, 4):
|
||||
site.log_change(
|
||||
user=user,
|
||||
request_id=uuid.uuid4(),
|
||||
action=2
|
||||
)
|
||||
|
||||
def test_objectchange_list(self):
|
||||
|
||||
url = reverse('extras:objectchange_list')
|
||||
params = {
|
||||
"user": User.objects.first(),
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_objectchange(self):
|
||||
|
||||
objectchange = ObjectChange.objects.first()
|
||||
response = self.client.get(objectchange.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -7,19 +7,19 @@ urlpatterns = [
|
||||
|
||||
# Tags
|
||||
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
|
||||
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
|
||||
# Config contexts
|
||||
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||
url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
|
||||
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
|
||||
|
||||
# Image attachments
|
||||
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||
|
||||
@@ -82,7 +82,7 @@ class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
|
||||
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
permission_required = 'taggit.delete_tag'
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('taggit_taggeditem_items')
|
||||
).order_by(
|
||||
|
||||
@@ -45,7 +45,7 @@ def enqueue_webhooks(instance, action):
|
||||
"extras.webhooks_worker.process_webhook",
|
||||
webhook,
|
||||
serializer.data,
|
||||
instance.__class__,
|
||||
instance._meta.model_name,
|
||||
action,
|
||||
str(datetime.datetime.now())
|
||||
)
|
||||
|
||||
@@ -10,14 +10,14 @@ from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJ
|
||||
|
||||
|
||||
@job('default')
|
||||
def process_webhook(webhook, data, model_class, event, timestamp):
|
||||
def process_webhook(webhook, data, model_name, event, timestamp):
|
||||
"""
|
||||
Make a POST request to the defined Webhook
|
||||
"""
|
||||
payload = {
|
||||
'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
|
||||
'timestamp': timestamp,
|
||||
'model': model_class._meta.model_name,
|
||||
'model': model_name,
|
||||
'data': data
|
||||
}
|
||||
headers = {
|
||||
|
||||
@@ -87,7 +87,7 @@ class VLANGroupSerializer(ValidatedModelSerializer):
|
||||
validator(data)
|
||||
|
||||
# Enforce model validation
|
||||
super(VLANGroupSerializer, self).validate(data)
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
@@ -118,7 +118,7 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
validator(data)
|
||||
|
||||
# Enforce model validation
|
||||
super(VLANSerializer, self).validate(data)
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class BaseIPField(models.Field):
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': self.form_class()}
|
||||
defaults.update(kwargs)
|
||||
return super(BaseIPField, self).formfield(**defaults)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class IPNetworkField(BaseIPField):
|
||||
|
||||
@@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
|
||||
from dcim.models import Site, Device, Interface
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NumericInFilter, TagFilter
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||
from virtualization.models import VirtualMachine
|
||||
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
@@ -48,7 +48,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
fields = ['name', 'rd', 'enforce_unique']
|
||||
|
||||
|
||||
class RIRFilter(django_filters.FilterSet):
|
||||
class RIRFilter(NameSlugSearchFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
@@ -96,7 +96,11 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class RoleFilter(django_filters.FilterSet):
|
||||
class RoleFilter(NameSlugSearchFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
@@ -112,6 +116,10 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
prefix = django_filters.CharFilter(
|
||||
method='filter_prefix',
|
||||
label='Prefix',
|
||||
)
|
||||
within = django_filters.CharFilter(
|
||||
method='search_within',
|
||||
label='Within prefix',
|
||||
@@ -197,6 +205,15 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_prefix(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
try:
|
||||
query = str(netaddr.IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix=query)
|
||||
except ValidationError:
|
||||
return queryset.none()
|
||||
|
||||
def search_within(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
@@ -360,7 +377,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class VLANGroupFilter(django_filters.FilterSet):
|
||||
class VLANGroupFilter(NameSlugSearchFilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
18
netbox/ipam/migrations/0024_vrf_allow_null_rd.py
Normal file
18
netbox/ipam/migrations/0024_vrf_allow_null_rd.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.1.5 on 2019-01-31 18:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0023_change_logging'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='vrf',
|
||||
name='rd',
|
||||
field=models.CharField(blank=True, max_length=21, null=True, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
import netaddr
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
@@ -10,8 +10,9 @@ from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.models import Interface
|
||||
from extras.models import CustomFieldModel
|
||||
from extras.models import CustomFieldModel, ObjectChange
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object
|
||||
from .constants import *
|
||||
from .fields import IPNetworkField, IPAddressField
|
||||
from .querysets import PrefixQuerySet
|
||||
@@ -29,6 +30,8 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
||||
rd = models.CharField(
|
||||
max_length=21,
|
||||
unique=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='Route distinguisher'
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
@@ -63,7 +66,7 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
||||
verbose_name_plural = 'VRFs'
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super(VRF, self).__str__()
|
||||
return self.display_name or super().__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:vrf', args=[self.pk])
|
||||
@@ -79,9 +82,9 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
if self.name and self.rd:
|
||||
if self.rd:
|
||||
return "{} ({})".format(self.name, self.rd)
|
||||
return None
|
||||
return self.name
|
||||
|
||||
|
||||
class RIR(ChangeLoggedModel):
|
||||
@@ -198,7 +201,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
||||
if self.prefix:
|
||||
# Infer address family from IPNetwork object
|
||||
self.family = self.prefix.version
|
||||
super(Aggregate, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@@ -369,7 +372,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
||||
self.prefix = self.prefix.cidr
|
||||
# Infer address family from IPNetwork object
|
||||
self.family = self.prefix.version
|
||||
super(Prefix, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@@ -385,6 +388,15 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
||||
self.description,
|
||||
)
|
||||
|
||||
def _set_prefix_length(self, value):
|
||||
"""
|
||||
Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,
|
||||
e.g. for bulk editing.
|
||||
"""
|
||||
if self.prefix is not None:
|
||||
self.prefix.prefixlen = value
|
||||
prefix_length = property(fset=_set_prefix_length)
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
@@ -429,12 +441,23 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
||||
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
|
||||
available_ips = prefix - child_ips
|
||||
|
||||
# Remove unusable IPs from non-pool prefixes
|
||||
if not self.is_pool:
|
||||
available_ips -= netaddr.IPSet([
|
||||
netaddr.IPAddress(self.prefix.first),
|
||||
netaddr.IPAddress(self.prefix.last),
|
||||
])
|
||||
# All IP addresses within a pool are considered usable
|
||||
if self.is_pool:
|
||||
return available_ips
|
||||
|
||||
# All IP addresses within a point-to-point prefix (IPv4 /31 or IPv6 /127) are considered usable
|
||||
if (
|
||||
self.family == 4 and self.prefix.prefixlen == 31 # RFC 3021
|
||||
) or (
|
||||
self.family == 6 and self.prefix.prefixlen == 127 # RFC 6164
|
||||
):
|
||||
return available_ips
|
||||
|
||||
# Omit first and last IP address from the available set
|
||||
available_ips -= netaddr.IPSet([
|
||||
netaddr.IPAddress(self.prefix.first),
|
||||
netaddr.IPAddress(self.prefix.last),
|
||||
])
|
||||
|
||||
return available_ips
|
||||
|
||||
@@ -484,7 +507,7 @@ class IPAddressManager(models.Manager):
|
||||
then re-cast this value to INET() so that records will be ordered properly. We are essentially re-casting each
|
||||
IP address as a /32 or /128.
|
||||
"""
|
||||
qs = super(IPAddressManager, self).get_queryset()
|
||||
qs = super().get_queryset()
|
||||
return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
|
||||
|
||||
|
||||
@@ -605,7 +628,28 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
if self.address:
|
||||
# Infer address family from IPAddress object
|
||||
self.family = self.address.version
|
||||
super(IPAddress, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def log_change(self, user, request_id, action):
|
||||
"""
|
||||
Include the connected Interface (if any).
|
||||
"""
|
||||
|
||||
# It's possible that an IPAddress can be deleted _after_ its parent Interface, in which case trying to resolve
|
||||
# the interface will raise DoesNotExist.
|
||||
try:
|
||||
parent_obj = self.interface
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=self,
|
||||
related_object=parent_obj,
|
||||
action=action,
|
||||
object_data=serialize_object(self)
|
||||
).save()
|
||||
|
||||
def to_csv(self):
|
||||
|
||||
@@ -630,6 +674,15 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
self.description,
|
||||
)
|
||||
|
||||
def _set_mask_length(self, value):
|
||||
"""
|
||||
Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,
|
||||
e.g. for bulk editing.
|
||||
"""
|
||||
if self.address is not None:
|
||||
self.address.prefixlen = value
|
||||
mask_length = property(fset=_set_mask_length)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
if self.interface:
|
||||
@@ -773,7 +826,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
||||
verbose_name_plural = 'VLANs'
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super(VLAN, self).__str__()
|
||||
return self.display_name or super().__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:vlan', args=[self.pk])
|
||||
@@ -812,7 +865,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
||||
return Interface.objects.filter(
|
||||
Q(untagged_vlan_id=self.pk) |
|
||||
Q(tagged_vlans=self.pk)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
|
||||
class Service(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
@@ -203,7 +203,7 @@ class RIRTable(BaseTable):
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
is_private = BooleanColumn(verbose_name='Private')
|
||||
aggregate_count = tables.Column(verbose_name='Aggregates')
|
||||
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
@@ -288,7 +288,7 @@ class RoleTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='VLANs'
|
||||
)
|
||||
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Role
|
||||
@@ -392,7 +392,7 @@ class VLANGroupTable(BaseTable):
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
vlan_count = tables.Column(verbose_name='VLANs')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -430,14 +430,14 @@ class VLANDetailTable(VLANTable):
|
||||
|
||||
class VLANMemberTable(BaseTable):
|
||||
parent = tables.LinkColumn(order_by=['device', 'virtual_machine'])
|
||||
name = tables.Column(verbose_name='Interface')
|
||||
name = tables.LinkColumn(verbose_name='Interface')
|
||||
untagged = tables.TemplateColumn(
|
||||
template_code=VLAN_MEMBER_UNTAGGED,
|
||||
orderable=False
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=VLAN_MEMBER_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
@@ -464,7 +464,7 @@ class InterfaceVLANTable(BaseTable):
|
||||
|
||||
def __init__(self, interface, *args, **kwargs):
|
||||
self.interface = interface
|
||||
super(InterfaceVLANTable, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -12,11 +12,11 @@ class VRFTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(VRFTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
|
||||
self.vrf2 = VRF.objects.create(name='Test VRF 2', rd='65000:2')
|
||||
self.vrf3 = VRF.objects.create(name='Test VRF 3', rd='65000:3')
|
||||
self.vrf3 = VRF.objects.create(name='Test VRF 3') # No RD
|
||||
|
||||
def test_get_vrf(self):
|
||||
|
||||
@@ -44,19 +44,26 @@ class VRFTest(APITestCase):
|
||||
|
||||
def test_create_vrf(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test VRF 4',
|
||||
'rd': '65000:4',
|
||||
}
|
||||
data_list = [
|
||||
# VRF with RD
|
||||
{
|
||||
'name': 'Test VRF 4',
|
||||
'rd': '65000:4',
|
||||
},
|
||||
# VRF without RD
|
||||
{
|
||||
'name': 'Test VRF 5',
|
||||
}
|
||||
]
|
||||
|
||||
url = reverse('ipam-api:vrf-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(VRF.objects.count(), 4)
|
||||
vrf4 = VRF.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(vrf4.name, data['name'])
|
||||
self.assertEqual(vrf4.rd, data['rd'])
|
||||
for data in data_list:
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
vrf = VRF.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(vrf.name, data['name'])
|
||||
self.assertEqual(vrf.rd, data['rd'] if 'rd' in data else None)
|
||||
|
||||
def test_create_vrf_bulk(self):
|
||||
|
||||
@@ -113,7 +120,7 @@ class RIRTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(RIRTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.rir1 = RIR.objects.create(name='Test RIR 1', slug='test-rir-1')
|
||||
self.rir2 = RIR.objects.create(name='Test RIR 2', slug='test-rir-2')
|
||||
@@ -214,7 +221,7 @@ class AggregateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(AggregateTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.rir1 = RIR.objects.create(name='Test RIR 1', slug='test-rir-1')
|
||||
self.rir2 = RIR.objects.create(name='Test RIR 2', slug='test-rir-2')
|
||||
@@ -317,7 +324,7 @@ class RoleTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(RoleTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.role1 = Role.objects.create(name='Test Role 1', slug='test-role-1')
|
||||
self.role2 = Role.objects.create(name='Test Role 2', slug='test-role-2')
|
||||
@@ -418,7 +425,7 @@ class PrefixTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(PrefixTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
|
||||
@@ -657,7 +664,7 @@ class IPAddressTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(IPAddressTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
|
||||
self.ipaddress1 = IPAddress.objects.create(address=IPNetwork('192.168.0.1/24'))
|
||||
@@ -756,7 +763,7 @@ class VLANGroupTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(VLANGroupTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.vlangroup1 = VLANGroup.objects.create(name='Test VLAN Group 1', slug='test-vlan-group-1')
|
||||
self.vlangroup2 = VLANGroup.objects.create(name='Test VLAN Group 2', slug='test-vlan-group-2')
|
||||
@@ -857,7 +864,7 @@ class VLANTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(VLANTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.vlan1 = VLAN.objects.create(vid=1, name='Test VLAN 1')
|
||||
self.vlan2 = VLAN.objects.create(vid=2, name='Test VLAN 2')
|
||||
@@ -958,7 +965,7 @@ class ServiceTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ServiceTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
|
||||
282
netbox/ipam/tests/test_views.py
Normal file
282
netbox/ipam/tests/test_views.py
Normal file
@@ -0,0 +1,282 @@
|
||||
from netaddr import IPNetwork
|
||||
import urllib.parse
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from ipam.constants import IP_PROTOCOL_TCP
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
class VRFTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
VRF.objects.bulk_create([
|
||||
VRF(name='VRF 1', rd='65000:1'),
|
||||
VRF(name='VRF 2', rd='65000:2'),
|
||||
VRF(name='VRF 3', rd='65000:3'),
|
||||
])
|
||||
|
||||
def test_vrf_list(self):
|
||||
|
||||
url = reverse('ipam:vrf_list')
|
||||
params = {
|
||||
"q": "65000",
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_configcontext(self):
|
||||
|
||||
vrf = VRF.objects.first()
|
||||
response = self.client.get(vrf.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RIRTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
RIR.objects.bulk_create([
|
||||
RIR(name='RIR 1', slug='rir-1'),
|
||||
RIR(name='RIR 2', slug='rir-2'),
|
||||
RIR(name='RIR 3', slug='rir-3'),
|
||||
])
|
||||
|
||||
def test_rir_list(self):
|
||||
|
||||
url = reverse('ipam:rir_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_rir(self):
|
||||
|
||||
rir = RIR.objects.first()
|
||||
response = self.client.get(rir.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class AggregateTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
rir = RIR(name='RIR 1', slug='rir-1')
|
||||
rir.save()
|
||||
|
||||
Aggregate.objects.bulk_create([
|
||||
Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir),
|
||||
Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rir),
|
||||
Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir),
|
||||
])
|
||||
|
||||
def test_aggregate_list(self):
|
||||
|
||||
url = reverse('ipam:aggregate_list')
|
||||
params = {
|
||||
"rir": RIR.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_aggregate(self):
|
||||
|
||||
aggregate = Aggregate.objects.first()
|
||||
response = self.client.get(aggregate.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class RoleTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
Role.objects.bulk_create([
|
||||
Role(name='Role 1', slug='role-1'),
|
||||
Role(name='Role 2', slug='role-2'),
|
||||
Role(name='Role 3', slug='role-3'),
|
||||
])
|
||||
|
||||
def test_role_list(self):
|
||||
|
||||
url = reverse('ipam:role_list')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class PrefixTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
Prefix.objects.bulk_create([
|
||||
Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site),
|
||||
Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), site=site),
|
||||
Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site),
|
||||
])
|
||||
|
||||
def test_prefix_list(self):
|
||||
|
||||
url = reverse('ipam:prefix_list')
|
||||
params = {
|
||||
"site": Site.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_prefix(self):
|
||||
|
||||
prefix = Prefix.objects.first()
|
||||
response = self.client.get(prefix.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class IPAddressTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
vrf = VRF(name='VRF 1', rd='65000:1')
|
||||
vrf.save()
|
||||
|
||||
IPAddress.objects.bulk_create([
|
||||
IPAddress(family=4, address=IPNetwork('10.1.0.0/16'), vrf=vrf),
|
||||
IPAddress(family=4, address=IPNetwork('10.2.0.0/16'), vrf=vrf),
|
||||
IPAddress(family=4, address=IPNetwork('10.3.0.0/16'), vrf=vrf),
|
||||
])
|
||||
|
||||
def test_ipaddress_list(self):
|
||||
|
||||
url = reverse('ipam:ipaddress_list')
|
||||
params = {
|
||||
"vrf": VRF.objects.first().rd,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_ipaddress(self):
|
||||
|
||||
ipaddress = IPAddress.objects.first()
|
||||
response = self.client.get(ipaddress.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class VLANGroupTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
VLANGroup.objects.bulk_create([
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
|
||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=site),
|
||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
|
||||
])
|
||||
|
||||
def test_vlangroup_list(self):
|
||||
|
||||
url = reverse('ipam:vlangroup_list')
|
||||
params = {
|
||||
"site": Site.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class VLANTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
|
||||
vlangroup.save()
|
||||
|
||||
VLAN.objects.bulk_create([
|
||||
VLAN(group=vlangroup, vid=101, name='VLAN101'),
|
||||
VLAN(group=vlangroup, vid=102, name='VLAN102'),
|
||||
VLAN(group=vlangroup, vid=103, name='VLAN103'),
|
||||
])
|
||||
|
||||
def test_vlan_list(self):
|
||||
|
||||
url = reverse('ipam:vlan_list')
|
||||
params = {
|
||||
"group": VLANGroup.objects.first().slug,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_vlan(self):
|
||||
|
||||
vlan = VLAN.objects.first()
|
||||
response = self.client.get(vlan.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ServiceTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.client = Client()
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
|
||||
manufacturer.save()
|
||||
|
||||
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
|
||||
devicetype.save()
|
||||
|
||||
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
|
||||
devicerole.save()
|
||||
|
||||
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
|
||||
device.save()
|
||||
|
||||
Service.objects.bulk_create([
|
||||
Service(device=device, name='Service 1', protocol=IP_PROTOCOL_TCP, port=101),
|
||||
Service(device=device, name='Service 2', protocol=IP_PROTOCOL_TCP, port=102),
|
||||
Service(device=device, name='Service 3', protocol=IP_PROTOCOL_TCP, port=103),
|
||||
])
|
||||
|
||||
def test_service_list(self):
|
||||
|
||||
url = reverse('ipam:service_list')
|
||||
params = {
|
||||
"device_id": Device.objects.first(),
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_service(self):
|
||||
|
||||
service = Service.objects.first()
|
||||
response = self.client.get(service.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -126,14 +126,11 @@ class VRFView(View):
|
||||
def get(self, request, pk):
|
||||
|
||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||
prefix_table = tables.PrefixTable(
|
||||
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
|
||||
)
|
||||
prefix_table.exclude = ('vrf',)
|
||||
prefix_count = Prefix.objects.filter(vrf=vrf).count()
|
||||
|
||||
return render(request, 'ipam/vrf.html', {
|
||||
'vrf': vrf,
|
||||
'prefix_table': prefix_table,
|
||||
'prefix_count': prefix_count,
|
||||
})
|
||||
|
||||
|
||||
@@ -715,7 +712,7 @@ class IPAddressAssignView(PermissionRequiredMixin, View):
|
||||
if 'interface' not in request.GET:
|
||||
return redirect('ipam:ipaddress_add')
|
||||
|
||||
return super(IPAddressAssignView, self).dispatch(request, *args, **kwargs)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request):
|
||||
|
||||
|
||||
@@ -58,14 +58,14 @@ class TokenPermissions(DjangoModelPermissions):
|
||||
def __init__(self):
|
||||
# LOGIN_REQUIRED determines whether read-only access is provided to anonymous users.
|
||||
self.authenticated_users_only = settings.LOGIN_REQUIRED
|
||||
super(TokenPermissions, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
def has_permission(self, request, view):
|
||||
# If token authentication is in use, verify that the token allows write operations (for unsafe methods).
|
||||
if request.method not in SAFE_METHODS and isinstance(request.auth, Token):
|
||||
if not request.auth.write_enabled:
|
||||
return False
|
||||
return super(TokenPermissions, self).has_permission(request, view)
|
||||
return super().has_permission(request, view)
|
||||
|
||||
|
||||
#
|
||||
@@ -132,7 +132,7 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
||||
if not self.limit:
|
||||
return None
|
||||
|
||||
return super(OptionalLimitOffsetPagination, self).get_next_link()
|
||||
return super().get_next_link()
|
||||
|
||||
def get_previous_link(self):
|
||||
|
||||
@@ -140,7 +140,7 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
||||
if not self.limit:
|
||||
return None
|
||||
|
||||
return super(OptionalLimitOffsetPagination, self).get_previous_link()
|
||||
return super().get_previous_link()
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -15,6 +15,7 @@ OBJ_TYPE_CHOICES = (
|
||||
('devicetype', 'Device types'),
|
||||
('device', 'Devices'),
|
||||
('virtualchassis', 'Virtual Chassis'),
|
||||
('cable', 'Cables'),
|
||||
)),
|
||||
('IPAM', (
|
||||
('vrf', 'VRFs'),
|
||||
|
||||
@@ -7,7 +7,7 @@ import warnings
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
# Check for Python 3.5+
|
||||
# Django 2.1 requires Python 3.5+
|
||||
if sys.version_info < (3, 5):
|
||||
raise RuntimeError(
|
||||
"NetBox requires Python 3.5 or higher (current: Python {})".format(sys.version.split()[0])
|
||||
@@ -21,7 +21,8 @@ except ImportError:
|
||||
"Configuration file is not present. Please define netbox/netbox/configuration.py per the documentation."
|
||||
)
|
||||
|
||||
VERSION = '2.5-beta2'
|
||||
|
||||
VERSION = '2.5.8'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -196,7 +197,7 @@ ROOT_URLCONF = 'netbox.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR + '/templates/'],
|
||||
'DIRS': [BASE_DIR + '/templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@@ -222,7 +223,7 @@ USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_ROOT = BASE_DIR + '/static/'
|
||||
STATIC_ROOT = BASE_DIR + '/static'
|
||||
STATIC_URL = '/{}static/'.format(BASE_PATH)
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, "project-static"),
|
||||
@@ -245,9 +246,17 @@ LOGIN_URL = '/{}login/'.format(BASE_PATH)
|
||||
# Secrets
|
||||
SECRETS_MIN_PUBKEY_SIZE = 2048
|
||||
|
||||
# Pagination
|
||||
PER_PAGE_DEFAULTS = [
|
||||
25, 50, 100, 250, 500, 1000
|
||||
]
|
||||
if PAGINATE_COUNT not in PER_PAGE_DEFAULTS:
|
||||
PER_PAGE_DEFAULTS.append(PAGINATE_COUNT)
|
||||
PER_PAGE_DEFAULTS = sorted(PER_PAGE_DEFAULTS)
|
||||
|
||||
# Django filters
|
||||
FILTERS_NULL_CHOICE_LABEL = 'None'
|
||||
FILTERS_NULL_CHOICE_VALUE = '0' # Must be a string
|
||||
FILTERS_NULL_CHOICE_VALUE = 'null'
|
||||
|
||||
# Django REST framework (API)
|
||||
REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version
|
||||
@@ -287,9 +296,12 @@ RQ_QUEUES = {
|
||||
|
||||
# drf_yasg settings for Swagger
|
||||
SWAGGER_SETTINGS = {
|
||||
'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema',
|
||||
'DEFAULT_FIELD_INSPECTORS': [
|
||||
'utilities.custom_inspectors.NullableBooleanFieldInspector',
|
||||
'utilities.custom_inspectors.CustomChoiceFieldInspector',
|
||||
'utilities.custom_inspectors.TagListFieldInspector',
|
||||
'utilities.custom_inspectors.SerializedPKRelatedFieldInspector',
|
||||
'drf_yasg.inspectors.CamelCaseJSONFilter',
|
||||
'drf_yasg.inspectors.ReferencingSerializerInspector',
|
||||
'drf_yasg.inspectors.RelatedFieldInspector',
|
||||
@@ -303,6 +315,7 @@ SWAGGER_SETTINGS = {
|
||||
'utilities.custom_inspectors.IdInFilterInspector',
|
||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||
],
|
||||
'DEFAULT_MODEL_DEPTH': 1,
|
||||
'DEFAULT_PAGINATOR_INSPECTORS': [
|
||||
'utilities.custom_inspectors.NullablePaginatorInspector',
|
||||
'drf_yasg.inspectors.DjangoRestResponsePagination',
|
||||
|
||||
@@ -11,13 +11,13 @@ from circuits.filters import CircuitFilter, ProviderFilter
|
||||
from circuits.models import Circuit, Provider
|
||||
from circuits.tables import CircuitTable, ProviderTable
|
||||
from dcim.filters import (
|
||||
DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
|
||||
CableFilter, DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
|
||||
)
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, Device, DeviceType, Interface, PowerPort, Rack, RackGroup, Site, VirtualChassis
|
||||
)
|
||||
from dcim.tables import (
|
||||
DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
|
||||
CableTable, DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
|
||||
)
|
||||
from extras.models import ObjectChange, ReportResult, TopologyMap
|
||||
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||
@@ -88,6 +88,12 @@ SEARCH_TYPES = OrderedDict((
|
||||
'table': VirtualChassisTable,
|
||||
'url': 'dcim:virtualchassis_list',
|
||||
}),
|
||||
('cable', {
|
||||
'queryset': Cable.objects.all(),
|
||||
'filter': CableFilter,
|
||||
'table': CableTable,
|
||||
'url': 'dcim:cable_list',
|
||||
}),
|
||||
# IPAM
|
||||
('vrf', {
|
||||
'queryset': VRF.objects.select_related('tenant'),
|
||||
@@ -261,6 +267,7 @@ class SearchView(View):
|
||||
class APIRootView(APIView):
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
swagger_schema = None
|
||||
|
||||
def get_view_name(self):
|
||||
return "API Root"
|
||||
|
||||
7
netbox/project-static/clipboard-2.0.4.min.js
vendored
Executable file
7
netbox/project-static/clipboard-2.0.4.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
@@ -49,6 +49,19 @@ footer p {
|
||||
}
|
||||
}
|
||||
|
||||
/* Printer friendly CSS class and various fixes for printing. */
|
||||
@media print {
|
||||
body {
|
||||
padding-top: 0px;
|
||||
}
|
||||
a[href]:after {
|
||||
content: none !important;
|
||||
}
|
||||
.noprint {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapse the nav menu on displays less than 960px wide */
|
||||
@media (max-width: 959px) {
|
||||
.navbar-header {
|
||||
@@ -120,6 +133,117 @@ input[name="pk"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Color Selections */
|
||||
.color-selection-aa1409 {
|
||||
background-color: #aa1409;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-f44336 {
|
||||
background-color: #f44336;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-e91e63 {
|
||||
background-color: #e91e63;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-ffe4e1 {
|
||||
background-color: #ffe4e1;
|
||||
color: #000000;
|
||||
}
|
||||
.color-selection-ff66ff {
|
||||
background-color: #ff66ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-9c27b0 {
|
||||
background-color: #9c27b0;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-673ab7 {
|
||||
background-color: #673ab7;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-3f51b5 {
|
||||
background-color: #3f51b5;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-2196f3 {
|
||||
background-color: #2196f3;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-03a9f4 {
|
||||
background-color: #03a9f4;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-00bcd4 {
|
||||
background-color: #00bcd4;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-009688 {
|
||||
background-color: #009688;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-00ffff {
|
||||
background-color: #00ffff;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-2f6a31 {
|
||||
background-color: #2f6a31;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-4caf50 {
|
||||
background-color: #4caf50;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-8bc34a {
|
||||
background-color: #8bc34a;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-cddc39 {
|
||||
background-color: #cddc39;
|
||||
color: #000000;
|
||||
}
|
||||
.color-selection-ffeb3b {
|
||||
background-color: #ffeb3b;
|
||||
color: #000000;
|
||||
}
|
||||
.color-selection-ffc107 {
|
||||
background-color: #ffc107;
|
||||
color: #000000;
|
||||
}
|
||||
.color-selection-ff9800 {
|
||||
background-color: #ff9800;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-ff5722 {
|
||||
background-color: #ff5722;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-795548 {
|
||||
background-color: #795548;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-c0c0c0 {
|
||||
background-color: #c0c0c0;
|
||||
color: #000000;
|
||||
}
|
||||
.color-selection-9e9e9e {
|
||||
background-color: #9e9e9e;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-607d8b {
|
||||
background-color: #607d8b;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-111111 {
|
||||
background-color: #111111;
|
||||
color: #ffffff;
|
||||
}
|
||||
.color-selection-ffffff {
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
|
||||
/* Tables */
|
||||
th.pk, td.pk {
|
||||
padding-bottom: 6px;
|
||||
@@ -140,6 +264,9 @@ table.attr-table td:nth-child(1) {
|
||||
div.paginator {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
div.paginator form {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
nav ul.pagination {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px !important;
|
||||
@@ -461,4 +588,4 @@ td .progress {
|
||||
}
|
||||
textarea {
|
||||
font-family: Consolas, Lucida Console, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
// Pagination
|
||||
$('select#per_page').change(function() {
|
||||
this.form.submit();
|
||||
});
|
||||
|
||||
// "Toggle" checkbox for object lists (PK column)
|
||||
$('input:checkbox.toggle').click(function() {
|
||||
$(this).closest('table').find('input:checkbox[name=pk]').prop('checked', $(this).prop('checked'));
|
||||
@@ -62,135 +67,227 @@ $(document).ready(function() {
|
||||
form.submit();
|
||||
});
|
||||
|
||||
// API select widget
|
||||
$('select[filter-for]').change(function() {
|
||||
|
||||
// Resolve child field by ID specified in parent
|
||||
var child_names = $(this).attr('filter-for');
|
||||
var parent = this;
|
||||
|
||||
// allow more than one child
|
||||
$.each(child_names.split(" "), function(_, child_name){
|
||||
|
||||
var child_field = $('#id_' + child_name);
|
||||
var child_selected = child_field.val();
|
||||
|
||||
// Wipe out any existing options within the child field and create a default option
|
||||
child_field.empty();
|
||||
if (!child_field.attr('multiple')) {
|
||||
child_field.append($("<option></option>").attr("value", "").text("---------"));
|
||||
// Parse URLs which may contain variable refrences to other field values
|
||||
function parseURL(url) {
|
||||
var filter_regex = /\{\{([a-z_]+)\}\}/g;
|
||||
var match;
|
||||
var rendered_url = url;
|
||||
var filter_field;
|
||||
while (match = filter_regex.exec(url)) {
|
||||
filter_field = $('#id_' + match[1]);
|
||||
var custom_attr = $('option:selected', filter_field).attr('api-value');
|
||||
if (custom_attr) {
|
||||
rendered_url = rendered_url.replace(match[0], custom_attr);
|
||||
} else if (filter_field.val()) {
|
||||
rendered_url = rendered_url.replace(match[0], filter_field.val());
|
||||
} else if (filter_field.attr('nullable') == 'true') {
|
||||
rendered_url = rendered_url.replace(match[0], 'null');
|
||||
}
|
||||
}
|
||||
return rendered_url
|
||||
}
|
||||
|
||||
if ($(parent).val() || $(parent).attr('nullable') == 'true') {
|
||||
var api_url = child_field.attr('api-url') + '&limit=0&brief=1';
|
||||
var disabled_indicator = child_field.attr('disabled-indicator');
|
||||
var initial_value = child_field.attr('initial');
|
||||
var display_field = child_field.attr('display-field') || 'name';
|
||||
|
||||
// Determine the filter fields needed to make an API call
|
||||
var filter_regex = /\{\{([a-z_]+)\}\}/g;
|
||||
var match;
|
||||
var rendered_url = api_url;
|
||||
var filter_field;
|
||||
while (match = filter_regex.exec(api_url)) {
|
||||
filter_field = $('#id_' + match[1]);
|
||||
var custom_attr = $('option:selected', filter_field).attr('api-value');
|
||||
if (custom_attr) {
|
||||
rendered_url = rendered_url.replace(match[0], custom_attr);
|
||||
} else if (filter_field.val()) {
|
||||
rendered_url = rendered_url.replace(match[0], filter_field.val());
|
||||
} else if (filter_field.attr('nullable') == 'true') {
|
||||
rendered_url = rendered_url.replace(match[0], '0');
|
||||
}
|
||||
}
|
||||
|
||||
// Account for any conditional URL append strings
|
||||
$.each(child_field[0].attributes, function(index, attr){
|
||||
if (attr.name.includes("data-url-conditional-append-")){
|
||||
var conditional = attr.name.split("data-url-conditional-append-")[1].split("__");
|
||||
var field = $("#id_" + conditional[0]);
|
||||
var field_value = conditional[1];
|
||||
if ($('option:selected', field).attr('api-value') === field_value){
|
||||
rendered_url = rendered_url + attr.value;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If all URL variables have been replaced, make the API call
|
||||
if (rendered_url.search('{{') < 0) {
|
||||
console.log(child_name + ": Fetching " + rendered_url);
|
||||
$.ajax({
|
||||
url: rendered_url,
|
||||
dataType: 'json',
|
||||
success: function(response, status) {
|
||||
$.each(response.results, function(index, choice) {
|
||||
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]);
|
||||
if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) {
|
||||
option.attr("disabled", "disabled");
|
||||
} else if (choice.id == child_selected) {
|
||||
option.attr("selected", "selected");
|
||||
}
|
||||
child_field.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Trigger change event in case the child field is the parent of another field
|
||||
child_field.change();
|
||||
});
|
||||
// Assign color picker selection classes
|
||||
function colorPickerClassCopy(data, container) {
|
||||
if (data.element) {
|
||||
// Remove any existing color-selection classes
|
||||
$(container).attr('class', function(i, c) {
|
||||
return c.replace(/(^|\s)color-selection-\S+/g, '');
|
||||
});
|
||||
$(container).addClass($(data.element).attr("class"));
|
||||
}
|
||||
return data.text;
|
||||
}
|
||||
|
||||
// Color Picker
|
||||
$('.netbox-select2-color-picker').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
templateResult: colorPickerClassCopy,
|
||||
templateSelection: colorPickerClassCopy
|
||||
});
|
||||
|
||||
// Auto-complete tags
|
||||
function split_tags(val) {
|
||||
return val.split(/,\s*/);
|
||||
}
|
||||
$("#id_tags")
|
||||
.on("keydown", function(event) {
|
||||
if (event.keyCode === $.ui.keyCode.TAB &&
|
||||
$(this).autocomplete("instance").menu.active) {
|
||||
event.preventDefault();
|
||||
}
|
||||
})
|
||||
.autocomplete({
|
||||
source: function(request, response) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: netbox_api_path + 'extras/tags/',
|
||||
data: 'q=' + split_tags(request.term).pop(),
|
||||
success: function(data) {
|
||||
var choices = [];
|
||||
$.each(data.results, function (index, choice) {
|
||||
choices.push(choice.name);
|
||||
});
|
||||
response(choices);
|
||||
// Static choice selection
|
||||
$('.netbox-select2-static').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap"
|
||||
});
|
||||
|
||||
// API backed selection
|
||||
// Includes live search and chained fields
|
||||
// The `multiple` setting may be controled via a data-* attribute
|
||||
$('.netbox-select2-api').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
ajax: {
|
||||
delay: 500,
|
||||
|
||||
url: function(params) {
|
||||
var element = this[0];
|
||||
var url = parseURL(element.getAttribute("data-url"));
|
||||
|
||||
if (url.includes("{{")) {
|
||||
// URL is not fully rendered yet, abort the request
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
search: function() {
|
||||
// Need 3 or more characters to begin searching
|
||||
var term = split_tags(this.value).pop();
|
||||
if (term.length < 3) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
focus: function() {
|
||||
// prevent value inserted on focus
|
||||
return false;
|
||||
},
|
||||
select: function(event, ui) {
|
||||
var terms = split_tags(this.value);
|
||||
// remove the current input
|
||||
terms.pop();
|
||||
// add the selected item
|
||||
terms.push(ui.item.value);
|
||||
// add placeholder to get the comma-and-space at the end
|
||||
terms.push("");
|
||||
this.value = terms.join(", ");
|
||||
return false;
|
||||
return url;
|
||||
},
|
||||
|
||||
data: function(params) {
|
||||
var element = this[0];
|
||||
// Paging. Note that `params.page` indexes at 1
|
||||
var offset = (params.page - 1) * 50 || 0;
|
||||
// Base query params
|
||||
var parameters = {
|
||||
q: params.term,
|
||||
brief: 1,
|
||||
limit: 50,
|
||||
offset: offset,
|
||||
};
|
||||
|
||||
// filter-for fields from a chain
|
||||
var attr_name = "data-filter-for-" + $(element).attr("name");
|
||||
var form = $(element).closest('form');
|
||||
var filter_for_elements = form.find("select[" + attr_name + "]");
|
||||
|
||||
filter_for_elements.each(function(index, filter_for_element) {
|
||||
var param_name = $(filter_for_element).attr(attr_name);
|
||||
var value = $(filter_for_element).val();
|
||||
|
||||
if (param_name && value) {
|
||||
parameters[param_name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Conditional query params
|
||||
$.each(element.attributes, function(index, attr){
|
||||
if (attr.name.includes("data-conditional-query-param-")){
|
||||
var conditional = attr.name.split("data-conditional-query-param-")[1].split("__");
|
||||
var field = $("#id_" + conditional[0]);
|
||||
var field_value = conditional[1];
|
||||
|
||||
if ($('option:selected', field).attr('api-value') === field_value){
|
||||
var _val = attr.value.split("=");
|
||||
parameters[_val[0]] = _val[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Additional query params
|
||||
$.each(element.attributes, function(index, attr){
|
||||
if (attr.name.includes("data-additional-query-param-")){
|
||||
var param_name = attr.name.split("data-additional-query-param-")[1]
|
||||
parameters[param_name] = attr.value;
|
||||
}
|
||||
});
|
||||
|
||||
// This will handle params with multiple values (i.e. for list filter forms)
|
||||
return $.param(parameters, true);
|
||||
},
|
||||
|
||||
processResults: function (data) {
|
||||
var element = this.$element[0];
|
||||
var results = $.map(data.results, function (obj) {
|
||||
obj.text = obj[element.getAttribute('display-field')] || obj.name;
|
||||
obj.id = obj[element.getAttribute('value-field')] || obj.id;
|
||||
|
||||
if(element.getAttribute('disabled-indicator') && obj[element.getAttribute('disabled-indicator')]) {
|
||||
// The disabled-indicator equated to true, so we disable this option
|
||||
obj.disabled = true;
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
|
||||
// Handle the null option, but only add it once
|
||||
if (element.getAttribute('data-null-option') && data.previous === null) {
|
||||
var null_option = $(element).children()[0]
|
||||
results.unshift({
|
||||
id: null_option.value,
|
||||
text: null_option.text
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are more results to page
|
||||
var page = data.next !== null;
|
||||
return {
|
||||
results: results,
|
||||
pagination: {
|
||||
more: page
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API backed tags
|
||||
var tags = $('#id_tags');
|
||||
if (tags.length > 0 && tags.val().length > 0){
|
||||
tags = $('#id_tags').val().split(/,\s*/);
|
||||
} else {
|
||||
tags = [];
|
||||
}
|
||||
tag_objs = $.map(tags, function (tag) {
|
||||
return {
|
||||
id: tag,
|
||||
text: tag,
|
||||
selected: true
|
||||
}
|
||||
});
|
||||
// Replace the django issued text input with a select element
|
||||
$('#id_tags').replaceWith('<select name="tags" id="id_tags" class="form-control"></select>');
|
||||
$('#id_tags').select2({
|
||||
tags: true,
|
||||
data: tag_objs,
|
||||
multiple: true,
|
||||
allowClear: true,
|
||||
placeholder: "Tags",
|
||||
|
||||
ajax: {
|
||||
delay: 250,
|
||||
url: "/api/extras/tags/",
|
||||
|
||||
data: function(params) {
|
||||
// Paging. Note that `params.page` indexes at 1
|
||||
var offset = (params.page - 1) * 50 || 0;
|
||||
var parameters = {
|
||||
q: params.term,
|
||||
brief: 1,
|
||||
limit: 50,
|
||||
offset: offset,
|
||||
};
|
||||
return parameters;
|
||||
},
|
||||
|
||||
processResults: function (data) {
|
||||
var results = $.map(data.results, function (obj) {
|
||||
return {
|
||||
id: obj.name,
|
||||
text: obj.name
|
||||
}
|
||||
});
|
||||
|
||||
// Check if there are more results to page
|
||||
var page = data.next !== null;
|
||||
return {
|
||||
results: results,
|
||||
pagination: {
|
||||
more: page
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
$('#id_tags').closest('form').submit(function(event){
|
||||
// django-taggit can only accept a single comma seperated string value
|
||||
var value = $('#id_tags').val();
|
||||
if (value.length > 0){
|
||||
var final_tags = value.join(', ');
|
||||
$('#id_tags').val(null).trigger('change');
|
||||
var option = new Option(final_tags, final_tags, true, true);
|
||||
$('#id_tags').append(option).trigger('change');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ $(document).ready(function() {
|
||||
source: function(request, response) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: search_field.attr('data-source'),
|
||||
url: search_field.attr('data-source') + '?brief=1',
|
||||
data: search_key + '=' + request.term,
|
||||
success: function(data) {
|
||||
var choices = [];
|
||||
@@ -49,7 +49,7 @@ $(document).ready(function() {
|
||||
// Disable parent selection fields
|
||||
// $('select[filter-for="' + real_field.attr('name') + '"]').val('');
|
||||
},
|
||||
minLength: 4,
|
||||
minLength: 3,
|
||||
delay: 500
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
$(document).ready(function() {
|
||||
// Instantiate ClipboardJS on all copy buttons
|
||||
new ClipboardJS('button.copy-secret');
|
||||
|
||||
// Unlocking a secret
|
||||
$('button.unlock-secret').click(function(event) {
|
||||
@@ -45,6 +47,7 @@ $(document).ready(function() {
|
||||
console.log("Secret retrieved successfully");
|
||||
$('#secret_' + secret_id).text(response.plaintext);
|
||||
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
|
||||
$('button.copy-secret[secret-id=' + secret_id + ']').show();
|
||||
$('button.lock-secret[secret-id=' + secret_id + ']').show();
|
||||
} else {
|
||||
console.log("Secret was not decrypted. Prompt user for private key.");
|
||||
@@ -67,6 +70,7 @@ $(document).ready(function() {
|
||||
var secret_div = $('#secret_' + secret_id);
|
||||
secret_div.html('********');
|
||||
$('button.lock-secret[secret-id=' + secret_id + ']').hide();
|
||||
$('button.copy-secret[secret-id=' + secret_id + ']').hide();
|
||||
$('button.unlock-secret[secret-id=' + secret_id + ']').show();
|
||||
}
|
||||
|
||||
|
||||
21
netbox/project-static/select2-4.0.5/LICENSE.md
Executable file
21
netbox/project-static/select2-4.0.5/LICENSE.md
Executable file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
123
netbox/project-static/select2-4.0.5/README.md
Executable file
123
netbox/project-static/select2-4.0.5/README.md
Executable file
@@ -0,0 +1,123 @@
|
||||
Select2
|
||||
=======
|
||||
[![Build Status][travis-ci-image]][travis-ci-status]
|
||||
|
||||
Select2 is a jQuery-based replacement for select boxes. It supports searching,
|
||||
remote data sets, and pagination of results.
|
||||
|
||||
To get started, checkout examples and documentation at
|
||||
https://select2.org/
|
||||
|
||||
Use cases
|
||||
---------
|
||||
* Enhancing native selects with search.
|
||||
* Enhancing native selects with a better multi-select interface.
|
||||
* Loading data from JavaScript: easily load items via AJAX and have them
|
||||
searchable.
|
||||
* Nesting optgroups: native selects only support one level of nesting. Select2
|
||||
does not have this restriction.
|
||||
* Tagging: ability to add new items on the fly.
|
||||
* Working with large, remote datasets: ability to partially load a dataset based
|
||||
on the search term.
|
||||
* Paging of large datasets: easy support for loading more pages when the results
|
||||
are scrolled to the end.
|
||||
* Templating: support for custom rendering of results and selections.
|
||||
|
||||
Browser compatibility
|
||||
---------------------
|
||||
* IE 8+
|
||||
* Chrome 8+
|
||||
* Firefox 10+
|
||||
* Safari 3+
|
||||
* Opera 10.6+
|
||||
|
||||
Select2 is automatically tested on the following browsers.
|
||||
|
||||
[![Sauce Labs Test Status][saucelabs-matrix]][saucelabs-status]
|
||||
|
||||
Usage
|
||||
-----
|
||||
You can source Select2 directly from a CDN like [JSDliver][jsdelivr] or
|
||||
[CDNJS][cdnjs], [download it from this GitHub repo][releases], or use one of
|
||||
the integrations below.
|
||||
|
||||
Integrations
|
||||
------------
|
||||
Third party developers have created plugins for platforms which allow Select2 to be integrated more natively and quickly. For many platforms, additional plugins are not required because Select2 acts as a standard `<select>` box.
|
||||
|
||||
Plugins
|
||||
|
||||
* [Django]
|
||||
- [django-autocomplete-light]
|
||||
- [django-easy-select2]
|
||||
- [django-select2]
|
||||
* [Meteor] - [meteor-select2]
|
||||
* [Ruby on Rails][ruby-on-rails] - [select2-rails]
|
||||
* [Wicket] - [wicketstuff-select2]
|
||||
* [Yii 2][yii2] - [yii2-widget-select2]
|
||||
|
||||
Themes
|
||||
|
||||
- [Bootstrap 3][bootstrap3] - [select2-bootstrap-theme]
|
||||
- [Flat UI][flat-ui] - [select2-flat-theme]
|
||||
- [Metro UI][metro-ui] - [select2-metro]
|
||||
|
||||
Missing an integration? Modify this `README` and make a pull request back here to Select2 on GitHub.
|
||||
|
||||
Internationalization (i18n)
|
||||
---------------------------
|
||||
Select2 supports multiple languages by simply including the right language JS
|
||||
file (`dist/js/i18n/it.js`, `dist/js/i18n/nl.js`, etc.) after
|
||||
`dist/js/select2.js`.
|
||||
|
||||
Missing a language? Just copy `src/js/select2/i18n/en.js`, translate it, and
|
||||
make a pull request back to Select2 here on GitHub.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
The documentation for Select2 is available
|
||||
[through GitHub Pages][documentation] and is located within this repository
|
||||
in the [`docs` folder][documentation-folder].
|
||||
|
||||
Community
|
||||
---------
|
||||
You can find out about the different ways to get in touch with the Select2
|
||||
community at the [Select2 community page][community].
|
||||
|
||||
Copyright and license
|
||||
---------------------
|
||||
The license is available within the repository in the [LICENSE][license] file.
|
||||
|
||||
[cdnjs]: http://www.cdnjs.com/libraries/select2
|
||||
[community]: https://select2.org/getting-help
|
||||
[documentation]: https://select2.org
|
||||
[documentation-folder]: https://github.com/select2/select2/tree/master/docs
|
||||
[freenode]: https://freenode.net/
|
||||
[jsdelivr]: http://www.jsdelivr.com/#!select2
|
||||
[license]: LICENSE.md
|
||||
[releases]: https://github.com/select2/select2/releases
|
||||
[saucelabs-matrix]: https://saucelabs.com/browser-matrix/select2.svg
|
||||
[saucelabs-status]: https://saucelabs.com/u/select2
|
||||
[travis-ci-image]: https://img.shields.io/travis/select2/select2/master.svg
|
||||
[travis-ci-status]: https://travis-ci.org/select2/select2
|
||||
|
||||
[bootstrap3]: https://getbootstrap.com/
|
||||
[django]: https://www.djangoproject.com/
|
||||
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
|
||||
[django-easy-select2]: https://github.com/asyncee/django-easy-select2
|
||||
[django-select2]: https://github.com/applegrew/django-select2
|
||||
[flat-ui]: http://designmodo.github.io/Flat-UI/
|
||||
[meteor]: https://www.meteor.com/
|
||||
[meteor-select2]: https://github.com/nate-strauser/meteor-select2
|
||||
[metro-ui]: http://metroui.org.ua/
|
||||
[select2-metro]: http://metroui.org.ua/select2.html
|
||||
[ruby-on-rails]: http://rubyonrails.org/
|
||||
[select2-bootstrap-theme]: https://github.com/select2/select2-bootstrap-theme
|
||||
[select2-flat-theme]: https://github.com/techhysahil/select2-Flat_Theme
|
||||
[select2-rails]: https://github.com/argerim/select2-rails
|
||||
[vue.js]: http://vuejs.org/
|
||||
[select2-vue]: http://vuejs.org/examples/select2.html
|
||||
[wicket]: https://wicket.apache.org/
|
||||
[wicketstuff-select2]: https://github.com/wicketstuff/core/tree/master/select2-parent
|
||||
[yii2]: http://www.yiiframework.com/
|
||||
[yii2-widget-select2]: https://github.com/kartik-v/yii2-widget-select2
|
||||
484
netbox/project-static/select2-4.0.5/css/select2.css
Executable file
484
netbox/project-static/select2-4.0.5/css/select2.css
Executable file
@@ -0,0 +1,484 @@
|
||||
.select2-container {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
vertical-align: middle; }
|
||||
.select2-container .select2-selection--single {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 28px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||
display: block;
|
||||
padding-left: 8px;
|
||||
padding-right: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.select2-container .select2-selection--single .select2-selection__clear {
|
||||
position: relative; }
|
||||
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
|
||||
padding-right: 8px;
|
||||
padding-left: 20px; }
|
||||
.select2-container .select2-selection--multiple {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
min-height: 32px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
padding-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.select2-container .select2-search--inline {
|
||||
float: left; }
|
||||
.select2-container .select2-search--inline .select2-search__field {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
font-size: 100%;
|
||||
margin-top: 5px;
|
||||
padding: 0; }
|
||||
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none; }
|
||||
|
||||
.select2-dropdown {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -100000px;
|
||||
width: 100%;
|
||||
z-index: 1051; }
|
||||
|
||||
.select2-results {
|
||||
display: block; }
|
||||
|
||||
.select2-results__options {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
.select2-results__option {
|
||||
padding: 6px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-results__option[aria-selected] {
|
||||
cursor: pointer; }
|
||||
|
||||
.select2-container--open .select2-dropdown {
|
||||
left: 0; }
|
||||
|
||||
.select2-container--open .select2-dropdown--above {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--open .select2-dropdown--below {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-search--dropdown {
|
||||
display: block;
|
||||
padding: 4px; }
|
||||
.select2-search--dropdown .select2-search__field {
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none; }
|
||||
.select2-search--dropdown.select2-search--hide {
|
||||
display: none; }
|
||||
|
||||
.select2-close-mask {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
height: auto;
|
||||
width: auto;
|
||||
opacity: 0;
|
||||
z-index: 99;
|
||||
background-color: #fff;
|
||||
filter: alpha(opacity=0); }
|
||||
|
||||
.select2-hidden-accessible {
|
||||
border: 0 !important;
|
||||
clip: rect(0 0 0 0) !important;
|
||||
height: 1px !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
width: 1px !important; }
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
background-color: #fff;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
color: #444;
|
||||
line-height: 28px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||
color: #999; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
left: 1px;
|
||||
right: auto; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection--single {
|
||||
background-color: #eee;
|
||||
cursor: default; }
|
||||
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||
display: none; }
|
||||
|
||||
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px; }
|
||||
|
||||
.select2-container--default .select2-selection--multiple {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: text; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
width: 100%; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
|
||||
list-style: none; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__placeholder {
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
float: left; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
margin-right: 10px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #333; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||
float: right; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||
border: solid black 1px;
|
||||
outline: 0; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection--multiple {
|
||||
background-color: #eee;
|
||||
cursor: default; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
|
||||
display: none; }
|
||||
|
||||
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
border: 1px solid #aaa; }
|
||||
|
||||
.select2-container--default .select2-search--inline .select2-search__field {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: textfield; }
|
||||
|
||||
.select2-container--default .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto; }
|
||||
|
||||
.select2-container--default .select2-results__option[role=group] {
|
||||
padding: 0; }
|
||||
|
||||
.select2-container--default .select2-results__option[aria-disabled=true] {
|
||||
color: #999; }
|
||||
|
||||
.select2-container--default .select2-results__option[aria-selected=true] {
|
||||
background-color: #ddd; }
|
||||
|
||||
.select2-container--default .select2-results__option .select2-results__option {
|
||||
padding-left: 1em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
|
||||
padding-left: 0; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -2em;
|
||||
padding-left: 3em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -3em;
|
||||
padding-left: 4em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -4em;
|
||||
padding-left: 5em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -5em;
|
||||
padding-left: 6em; }
|
||||
|
||||
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #5897fb;
|
||||
color: white; }
|
||||
|
||||
.select2-container--default .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px; }
|
||||
|
||||
.select2-container--classic .select2-selection--single {
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
outline: 0;
|
||||
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||
.select2-container--classic .select2-selection--single:focus {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__rendered {
|
||||
color: #444;
|
||||
line-height: 28px; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin-right: 10px; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
|
||||
color: #999; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__arrow {
|
||||
background-color: #ddd;
|
||||
border: none;
|
||||
border-left: 1px solid #aaa;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
border: none;
|
||||
border-right: 1px solid #aaa;
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
left: 1px;
|
||||
right: auto; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-selection--single {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
|
||||
background: transparent;
|
||||
border: none; }
|
||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
|
||||
|
||||
.select2-container--classic .select2-selection--multiple {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: text;
|
||||
outline: 0; }
|
||||
.select2-container--classic .select2-selection--multiple:focus {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
|
||||
display: none; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #555; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
float: right; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-selection--multiple {
|
||||
border: 1px solid #5897fb; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--classic .select2-search--dropdown .select2-search__field {
|
||||
border: 1px solid #aaa;
|
||||
outline: 0; }
|
||||
|
||||
.select2-container--classic .select2-search--inline .select2-search__field {
|
||||
outline: 0;
|
||||
box-shadow: none; }
|
||||
|
||||
.select2-container--classic .select2-dropdown {
|
||||
background-color: white;
|
||||
border: 1px solid transparent; }
|
||||
|
||||
.select2-container--classic .select2-dropdown--above {
|
||||
border-bottom: none; }
|
||||
|
||||
.select2-container--classic .select2-dropdown--below {
|
||||
border-top: none; }
|
||||
|
||||
.select2-container--classic .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto; }
|
||||
|
||||
.select2-container--classic .select2-results__option[role=group] {
|
||||
padding: 0; }
|
||||
|
||||
.select2-container--classic .select2-results__option[aria-disabled=true] {
|
||||
color: grey; }
|
||||
|
||||
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #3875d7;
|
||||
color: white; }
|
||||
|
||||
.select2-container--classic .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-dropdown {
|
||||
border-color: #5897fb; }
|
||||
1
netbox/project-static/select2-4.0.5/css/select2.min.css
vendored
Executable file
1
netbox/project-static/select2-4.0.5/css/select2.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
3
netbox/project-static/select2-4.0.5/js/i18n/af.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/af.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/af",[],function(){return{errorLoading:function(){return"Die resultate kon nie gelaai word nie."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Verwyders asseblief "+t+" character";return t!=1&&(n+="s"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Voer asseblief "+t+" of meer karakters";return n},loadingMore:function(){return"Meer resultate word gelaai…"},maximumSelected:function(e){var t="Kies asseblief net "+e.maximum+" item";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"Geen resultate gevind"},searching:function(){return"Besig…"}}}),{define:e.define,require:e.require}})();
|
||||
3
netbox/project-static/select2-4.0.5/js/i18n/ar.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/ar.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ar",[],function(){return{errorLoading:function(){return"لا يمكن تحميل النتائج"},inputTooLong:function(e){var t=e.input.length-e.maximum;return"الرجاء حذف "+t+" عناصر"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"الرجاء إضافة "+t+" عناصر"},loadingMore:function(){return"جاري تحميل نتائج إضافية..."},maximumSelected:function(e){return"تستطيع إختيار "+e.maximum+" بنود فقط"},noResults:function(){return"لم يتم العثور على أي نتائج"},searching:function(){return"جاري البحث…"}}}),{define:e.define,require:e.require}})();
|
||||
3
netbox/project-static/select2-4.0.5/js/i18n/az.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/az.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/az",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return t+" simvol silin"},inputTooShort:function(e){var t=e.minimum-e.input.length;return t+" simvol daxil edin"},loadingMore:function(){return"Daha çox nəticə yüklənir…"},maximumSelected:function(e){return"Sadəcə "+e.maximum+" element seçə bilərsiniz"},noResults:function(){return"Nəticə tapılmadı"},searching:function(){return"Axtarılır…"}}}),{define:e.define,require:e.require}})();
|
||||
3
netbox/project-static/select2-4.0.5/js/i18n/bg.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/bg.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/bg",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Моля въведете с "+t+" по-малко символ";return t>1&&(n+="a"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Моля въведете още "+t+" символ";return t>1&&(n+="a"),n},loadingMore:function(){return"Зареждат се още…"},maximumSelected:function(e){var t="Можете да направите до "+e.maximum+" ";return e.maximum>1?t+="избора":t+="избор",t},noResults:function(){return"Няма намерени съвпадения"},searching:function(){return"Търсене…"}}}),{define:e.define,require:e.require}})();
|
||||
3
netbox/project-static/select2-4.0.5/js/i18n/bs.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/bs.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/bs",[],function(){function e(e,t,n,r){return e%10==1&&e%100!=11?t:e%10>=2&&e%10<=4&&(e%100<12||e%100>14)?n:r}return{errorLoading:function(){return"Preuzimanje nije uspijelo."},inputTooLong:function(t){var n=t.input.length-t.maximum,r="Obrišite "+n+" simbol";return r+=e(n,"","a","a"),r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Ukucajte bar još "+n+" simbol";return r+=e(n,"","a","a"),r},loadingMore:function(){return"Preuzimanje još rezultata…"},maximumSelected:function(t){var n="Možete izabrati samo "+t.maximum+" stavk";return n+=e(t.maximum,"u","e","i"),n},noResults:function(){return"Ništa nije pronađeno"},searching:function(){return"Pretraga…"}}}),{define:e.define,require:e.require}})();
|
||||
3
netbox/project-static/select2-4.0.5/js/i18n/ca.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/ca.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ca",[],function(){return{errorLoading:function(){return"La càrrega ha fallat"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Si us plau, elimina "+t+" car";return t==1?n+="àcter":n+="àcters",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Si us plau, introdueix "+t+" car";return t==1?n+="àcter":n+="àcters",n},loadingMore:function(){return"Carregant més resultats…"},maximumSelected:function(e){var t="Només es pot seleccionar "+e.maximum+" element";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No s'han trobat resultats"},searching:function(){return"Cercant…"}}}),{define:e.define,require:e.require}})();
|
||||
3
netbox/project-static/select2-4.0.5/js/i18n/cs.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/cs.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/cs",[],function(){function e(e,t){switch(e){case 2:return t?"dva":"dvě";case 3:return"tři";case 4:return"čtyři"}return""}return{errorLoading:function(){return"Výsledky nemohly být načteny."},inputTooLong:function(t){var n=t.input.length-t.maximum;return n==1?"Prosím, zadejte o jeden znak méně.":n<=4?"Prosím, zadejte o "+e(n,!0)+" znaky méně.":"Prosím, zadejte o "+n+" znaků méně."},inputTooShort:function(t){var n=t.minimum-t.input.length;return n==1?"Prosím, zadejte ještě jeden znak.":n<=4?"Prosím, zadejte ještě další "+e(n,!0)+" znaky.":"Prosím, zadejte ještě dalších "+n+" znaků."},loadingMore:function(){return"Načítají se další výsledky…"},maximumSelected:function(t){var n=t.maximum;return n==1?"Můžete zvolit jen jednu položku.":n<=4?"Můžete zvolit maximálně "+e(n,!1)+" položky.":"Můžete zvolit maximálně "+n+" položek."},noResults:function(){return"Nenalezeny žádné položky."},searching:function(){return"Vyhledávání…"}}}),{define:e.define,require:e.require}})();
|
||||
3
netbox/project-static/select2-4.0.5/js/i18n/da.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/da.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/da",[],function(){return{errorLoading:function(){return"Resultaterne kunne ikke indlæses."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Angiv venligst "+t+" tegn mindre"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Angiv venligst "+t+" tegn mere"},loadingMore:function(){return"Indlæser flere resultater…"},maximumSelected:function(e){var t="Du kan kun vælge "+e.maximum+" emne";return e.maximum!=1&&(t+="r"),t},noResults:function(){return"Ingen resultater fundet"},searching:function(){return"Søger…"}}}),{define:e.define,require:e.require}})();
|
||||
3
netbox/project-static/select2-4.0.5/js/i18n/de.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/de.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/de",[],function(){return{errorLoading:function(){return"Die Ergebnisse konnten nicht geladen werden."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Bitte "+t+" Zeichen weniger eingeben"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Bitte "+t+" Zeichen mehr eingeben"},loadingMore:function(){return"Lade mehr Ergebnisse…"},maximumSelected:function(e){var t="Sie können nur "+e.maximum+" Eintr";return e.maximum===1?t+="ag":t+="äge",t+=" auswählen",t},noResults:function(){return"Keine Übereinstimmungen gefunden"},searching:function(){return"Suche…"}}}),{define:e.define,require:e.require}})();
|
||||
3
netbox/project-static/select2-4.0.5/js/i18n/dsb.js
Executable file
3
netbox/project-static/select2-4.0.5/js/i18n/dsb.js
Executable file
@@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.5 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/dsb",[],function(){var e=["znamuško","znamušce","znamuška","znamuškow"],t=["zapisk","zapiska","zapiski","zapiskow"],n=function(t,n){if(t===1)return n[0];if(t===2)return n[1];if(t>2&&t<=4)return n[2];if(t>=5)return n[3]};return{errorLoading:function(){return"Wuslědki njejsu se dali zacytaś."},inputTooLong:function(t){var r=t.input.length-t.maximum;return"Pšosym lašuj "+r+" "+n(r,e)},inputTooShort:function(t){var r=t.minimum-t.input.length;return"Pšosym zapódaj nanejmjenjej "+r+" "+n(r,e)},loadingMore:function(){return"Dalšne wuslědki se zacytaju…"},maximumSelected:function(e){return"Móžoš jano "+e.maximum+" "+n(e.maximum,t)+"wubraś."},noResults:function(){return"Žedne wuslědki namakane"},searching:function(){return"Pyta se…"}}}),{define:e.define,require:e.require}})();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user