mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-25 03:08:15 +01:00
Compare commits
256 Commits
v2.4.0
...
v2.5-beta1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f61cea695b | ||
|
|
e806b58ac9 | ||
|
|
338ce737c8 | ||
|
|
f41f141130 | ||
|
|
9f0169286b | ||
|
|
60452a7b0c | ||
|
|
6140dd955a | ||
|
|
5e06c5a9ea | ||
|
|
ad4e1f01aa | ||
|
|
bb114ea66b | ||
|
|
ce00226bc6 | ||
|
|
8bad25b860 | ||
|
|
99edb8b8d5 | ||
|
|
b4998f4b01 | ||
|
|
51295389c6 | ||
|
|
5baf86dc89 | ||
|
|
fd4a9db13e | ||
|
|
e243234c4e | ||
|
|
817dc89279 | ||
|
|
798a87b31e | ||
|
|
4d47d848c5 | ||
|
|
bd3ccfe020 | ||
|
|
ded90df01b | ||
|
|
ce7930abfd | ||
|
|
82b4aad585 | ||
|
|
f321e2c705 | ||
|
|
0c86fd89ca | ||
|
|
01628cdd31 | ||
|
|
e57b3bc4ab | ||
|
|
82076d3ca6 | ||
|
|
46e594f1f1 | ||
|
|
e8caa46484 | ||
|
|
1847e82d04 | ||
|
|
045856c9ac | ||
|
|
53b6a07a41 | ||
|
|
976388f29c | ||
|
|
e5f12109c5 | ||
|
|
319869c22e | ||
|
|
66ef5c726c | ||
|
|
f8a961b568 | ||
|
|
521a989d23 | ||
|
|
0a71c63583 | ||
|
|
b05171b126 | ||
|
|
9804db4551 | ||
|
|
516faac73f | ||
|
|
29d546a3f1 | ||
|
|
bb5c0989f3 | ||
|
|
3ea2978df4 | ||
|
|
cf9c8f35a0 | ||
|
|
cefec1be60 | ||
|
|
16d8308fa8 | ||
|
|
af95ba48aa | ||
|
|
43446cc642 | ||
|
|
c60c5502f6 | ||
|
|
4ffe1866c5 | ||
|
|
d4ec309d68 | ||
|
|
fa6c4db13b | ||
|
|
91ce6c2420 | ||
|
|
62da0778ee | ||
|
|
7ffb5f16dd | ||
|
|
623de7d210 | ||
|
|
1119209fa0 | ||
|
|
c3db7191d2 | ||
|
|
2af3685dae | ||
|
|
841ddd5d83 | ||
|
|
a5f0deaea3 | ||
|
|
105f9365bf | ||
|
|
903dfb6f07 | ||
|
|
571a3e9677 | ||
|
|
79c110df39 | ||
|
|
ad03a0925b | ||
|
|
5e7fccb64e | ||
|
|
3bc6fc5e24 | ||
|
|
849db8fe44 | ||
|
|
55c632ace7 | ||
|
|
cd243a90d0 | ||
|
|
4e9ace39ce | ||
|
|
18628166dc | ||
|
|
f7e23d912c | ||
|
|
121a6afb45 | ||
|
|
c2656c4ce1 | ||
|
|
d22c23290f | ||
|
|
8992c57039 | ||
|
|
939a52dcbd | ||
|
|
6f788fcd23 | ||
|
|
f2f9ff5cbd | ||
|
|
470aabe1d7 | ||
|
|
669fe1f192 | ||
|
|
62ca511619 | ||
|
|
10492de50d | ||
|
|
e1e9a451da | ||
|
|
60f5371b31 | ||
|
|
aad4f58633 | ||
|
|
2fa1c81070 | ||
|
|
3420b5fbdf | ||
|
|
4df74780b8 | ||
|
|
cbfb25f003 | ||
|
|
e3dc12338b | ||
|
|
e75ef5fd2d | ||
|
|
8ebf7a7ad0 | ||
|
|
d7766b9828 | ||
|
|
427226de8c | ||
|
|
cb1bdccf51 | ||
|
|
1ab4344537 | ||
|
|
17a71d3e26 | ||
|
|
05431aa83b | ||
|
|
e21b23cd98 | ||
|
|
dc67028bd9 | ||
|
|
8f8df2b8e8 | ||
|
|
8573c8b8cb | ||
|
|
33cdecbc52 | ||
|
|
0c7510c1a8 | ||
|
|
94946c5854 | ||
|
|
83dec3b587 | ||
|
|
3518d023dc | ||
|
|
4ad7882762 | ||
|
|
957de0cd98 | ||
|
|
6bea8cc546 | ||
|
|
2d90fc608e | ||
|
|
f134a6ec63 | ||
|
|
266429101b | ||
|
|
b19e2037af | ||
|
|
e79a156e1f | ||
|
|
35f80f5085 | ||
|
|
47c523a40b | ||
|
|
cca44d44f7 | ||
|
|
54c917beff | ||
|
|
9985f2cb82 | ||
|
|
f30367e094 | ||
|
|
2ec8dc8319 | ||
|
|
1595a5ecd7 | ||
|
|
ea0de629df | ||
|
|
471bddea09 | ||
|
|
e97708ada0 | ||
|
|
a36b120c8b | ||
|
|
4cfe9fe182 | ||
|
|
59af8cc924 | ||
|
|
d908dffab7 | ||
|
|
ea5121ffe1 | ||
|
|
0bb5d229e8 | ||
|
|
409a9256a1 | ||
|
|
2c37e85a4c | ||
|
|
0ae2dfbff3 | ||
|
|
df5d105f29 | ||
|
|
219c9e7d95 | ||
|
|
6832df4699 | ||
|
|
22ed4f1b53 | ||
|
|
c31c7b50b7 | ||
|
|
7d1f6b7049 | ||
|
|
c2f4cf3407 | ||
|
|
83f3dc99ce | ||
|
|
44d3606ce1 | ||
|
|
470d22c835 | ||
|
|
4c37628784 | ||
|
|
1daf7f8e2b | ||
|
|
841db3b0c2 | ||
|
|
5d10d8418e | ||
|
|
2fee977b4c | ||
|
|
52f1b1c3bf | ||
|
|
ab53d0e863 | ||
|
|
259da2d18a | ||
|
|
bf47e7cae3 | ||
|
|
db2721c581 | ||
|
|
1b2e9a6d06 | ||
|
|
99848aab6a | ||
|
|
3eddeeadc5 | ||
|
|
364bbdeab8 | ||
|
|
7583912de7 | ||
|
|
20fed375d1 | ||
|
|
fc1b3d6927 | ||
|
|
aed2a3cd1b | ||
|
|
15babeb584 | ||
|
|
020b5ea870 | ||
|
|
2ee5b2344e | ||
|
|
7616bcad3d | ||
|
|
f76ce980e3 | ||
|
|
9440ac7640 | ||
|
|
0e18997c79 | ||
|
|
95464772ac | ||
|
|
b4445dfdf8 | ||
|
|
fb5dca2711 | ||
|
|
6cdff955dc | ||
|
|
4039753b2f | ||
|
|
9df33cef8b | ||
|
|
e3e9211e8a | ||
|
|
0da113b723 | ||
|
|
e965adad7c | ||
|
|
57b225b680 | ||
|
|
b97597c645 | ||
|
|
162828da90 | ||
|
|
292647da14 | ||
|
|
3a88e43103 | ||
|
|
2ac60bdf48 | ||
|
|
010765e131 | ||
|
|
cde6e9757b | ||
|
|
f2d9a3e0a1 | ||
|
|
b917e8d3b0 | ||
|
|
a457a73826 | ||
|
|
ac36339491 | ||
|
|
dbbf7ab664 | ||
|
|
66400a98f1 | ||
|
|
aa50e2e385 | ||
|
|
118b8db209 | ||
|
|
e1e41a768a | ||
|
|
c333af33dc | ||
|
|
9e5b482b1d | ||
|
|
771747147c | ||
|
|
bc49979243 | ||
|
|
d46b3e2446 | ||
|
|
2804d89c5e | ||
|
|
19b737a534 | ||
|
|
29380b1120 | ||
|
|
fd32a71131 | ||
|
|
1556fd0e92 | ||
|
|
c4be440cd1 | ||
|
|
5dce7c4e48 | ||
|
|
4bfc32ec99 | ||
|
|
ff65f7fd7b | ||
|
|
7145f86a6e | ||
|
|
7ec4155845 | ||
|
|
6dd62dc891 | ||
|
|
5f66710fcd | ||
|
|
980d62d579 | ||
|
|
ac546a9711 | ||
|
|
cd2aee3053 | ||
|
|
9d9318f38a | ||
|
|
f43d861b50 | ||
|
|
17714b0c12 | ||
|
|
9914576eaa | ||
|
|
bf8eff11ea | ||
|
|
a6c78b99c4 | ||
|
|
6a56ffc650 | ||
|
|
05059606c5 | ||
|
|
a2ff21fab9 | ||
|
|
134370f48d | ||
|
|
c7fa610842 | ||
|
|
edb49c7f0a | ||
|
|
3e0a7e7f8a | ||
|
|
cfab9a6a0a | ||
|
|
91b5f6d799 | ||
|
|
d5488ca7da | ||
|
|
f9911bff0d | ||
|
|
d5239191fe | ||
|
|
db7148350e | ||
|
|
c51c20a301 | ||
|
|
f4485dc72a | ||
|
|
f59682a7c9 | ||
|
|
507a023f41 | ||
|
|
81479ac177 | ||
|
|
c7acddbc5c | ||
|
|
1905516536 | ||
|
|
64f34d9cd7 | ||
|
|
98bdb0cb3c | ||
|
|
bba88b2be4 | ||
|
|
12dfd4b6e0 | ||
|
|
209e721219 |
@@ -5,7 +5,6 @@ addons:
|
||||
postgresql: "9.4"
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.5"
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
|
||||
1771
CHANGELOG.md
Normal file
1771
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
17
README.md
17
README.md
@@ -16,8 +16,6 @@ or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode
|
||||
|
||||
### Build Status
|
||||
|
||||
NetBox is built against both Python 2.7 and 3.5. Python 3.5 or higher is strongly recommended.
|
||||
|
||||
| | status |
|
||||
|-------------|------------|
|
||||
| **master** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
@@ -42,3 +40,18 @@ and run `upgrade.sh`.
|
||||
* [Docker container](https://github.com/ninech/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))
|
||||
|
||||
# Related projects
|
||||
|
||||
## Supported SDK
|
||||
|
||||
- [pynetbox](https://github.com/digitalocean/pynetbox) Python API client library for Netbox.
|
||||
|
||||
## Community SDK
|
||||
|
||||
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) A ruby client library for Netbox v2.
|
||||
|
||||
## Ansible Inventory
|
||||
|
||||
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) Ansible dynamic inventory script for Netbox.
|
||||
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
# django-filter-1.1.0 breaks with Django-2.1
|
||||
Django>=1.11,<2.1
|
||||
Django
|
||||
django-cors-headers
|
||||
django-debug-toolbar
|
||||
# django-filter-2.0.0 drops Python 2 support (blocked by #2000)
|
||||
django-filter==1.1.0
|
||||
django-filter
|
||||
django-mptt
|
||||
django-tables2
|
||||
django-taggit
|
||||
django-taggit-serializer
|
||||
django-timezone-field
|
||||
# https://github.com/encode/django-rest-framework/issues/6053
|
||||
djangorestframework==3.8.1
|
||||
djangorestframework
|
||||
drf-yasg[validation]
|
||||
graphviz
|
||||
Markdown
|
||||
natsort
|
||||
ncclient
|
||||
# py-gfm requires Markdown<3.0
|
||||
Markdown<3.0
|
||||
netaddr
|
||||
paramiko
|
||||
Pillow
|
||||
psycopg2-binary
|
||||
py-gfm
|
||||
pycryptodome
|
||||
xmltodict
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Contextual Configuration Data
|
||||
|
||||
Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
|
||||
|
||||
Devices and Virtual Machines may also have a local config context defined. This local context will always overwrite the rendered config context objects for the Device/VM. This is useful in situations were the device requires a one-off value different from the rest of the environment.
|
||||
|
||||
@@ -9,7 +9,7 @@ This will launch a customized version of [the built-in Django shell](https://doc
|
||||
```
|
||||
$ ./manage.py nbshell
|
||||
### NetBox interactive shell (jstretch-laptop)
|
||||
### Python 2.7.6 | Django 1.11.3 | NetBox 2.1.0-dev
|
||||
### Python 3.5.2 | Django 2.0.8 | NetBox 2.4.3
|
||||
### lsmodels() will show available models. Use help(<model>) for more info.
|
||||
```
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@ A webhook defines an HTTP request that is sent to an external application when c
|
||||
|
||||
An optional secret key can be configured for each webhook. This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. This digest can be used by the receiver to authenticate the request's content.
|
||||
|
||||
## Installation
|
||||
|
||||
If you are upgrading from a previous version of Netbox and want to enable the webhook feature, please follow the directions listed in the sections below.
|
||||
|
||||
* [Install Redis server and djano-rq package](../installation/2-netbox/#install-python-packages)
|
||||
* [Modify configuration to enable webhooks](../installation/2-netbox/#webhooks-configuration)
|
||||
* [Create supervisord program to run the rqworker process](../installation/3-http-daemon/#supervisord-installation)
|
||||
|
||||
## Requests
|
||||
|
||||
The webhook POST request is structured as so (assuming `application/json` as the Content-Type):
|
||||
|
||||
@@ -7,10 +7,18 @@ NetBox uses [PostgreSQL](https://www.postgresql.org/) for its database, so gener
|
||||
|
||||
## Export the Database
|
||||
|
||||
Use the `pg_dump` utility to export the entire database to a file:
|
||||
|
||||
```no-highlight
|
||||
pg_dump netbox > netbox.sql
|
||||
```
|
||||
|
||||
When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
|
||||
|
||||
```no-highlight
|
||||
pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql
|
||||
```
|
||||
|
||||
## Load an Exported Database
|
||||
|
||||
!!! warning
|
||||
|
||||
@@ -4,6 +4,9 @@ The NetBox API employs token-based authentication. For convenience, cookie authe
|
||||
|
||||
A token is a unique identifier that identifies a user to the API. Each user in NetBox may have one or more tokens which he or she can use to authenticate to the API. To create a token, navigate to the API tokens page at `/user/api-tokens/`.
|
||||
|
||||
!!! note
|
||||
The creation and modification of API tokens can be restricted per user by an administrator. If you don't see an option to create an API token, ask an administrator to grant you access.
|
||||
|
||||
Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
|
||||
|
||||
By default, a token can be used for all operations available via the API. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
|
||||
|
||||
@@ -12,5 +12,5 @@ While NetBox has many configuration settings, only a few of them must be defined
|
||||
Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect:
|
||||
|
||||
```no-highlight
|
||||
# sudo supervsiorctl restart netbox
|
||||
# sudo supervisorctl restart netbox
|
||||
```
|
||||
|
||||
@@ -25,7 +25,7 @@ Circuit types are fully customizable.
|
||||
|
||||
A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
|
||||
|
||||
Each circuit termination is tied to a site, and optionally to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
|
||||
Each circuit termination is tied to a site, and may optionally be connected via a cable to a specific device interface or pass-through port. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
|
||||
|
||||
!!! note
|
||||
A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit.
|
||||
|
||||
@@ -4,12 +4,6 @@ A device type represents a particular make and model of hardware that exists in
|
||||
|
||||
Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type at the time of creation. (However, changes made to a device type will **not** apply to instances of that device type retroactively.)
|
||||
|
||||
The device type model includes three flags which inform what type of components may be added to it:
|
||||
|
||||
* `is_console_server`: This device type has console server ports
|
||||
* `is_pdu`: This device type has power outlets
|
||||
* `is_network_device`: This device type has network interfaces
|
||||
|
||||
Some devices house child devices which share physical resources, like space and power, but which functional independently from one another. A common example of this is blade server chassis. Each device type is designated as one of the following:
|
||||
|
||||
* A parent device (which has device bays)
|
||||
@@ -32,6 +26,8 @@ Each device type is assigned a number of component templates which define the ph
|
||||
* Power ports
|
||||
* Power outlets
|
||||
* Network interfaces
|
||||
* Front ports
|
||||
* Rear ports
|
||||
* Device bays (which house child devices)
|
||||
|
||||
Whenever a new device is created, its components are automatically created per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates defined:
|
||||
@@ -56,32 +52,28 @@ When assigning a multi-U device to a rack, it is considered to be mounted in the
|
||||
|
||||
A device is said to be full depth if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede airflow.
|
||||
|
||||
## Device Roles
|
||||
## Device Components
|
||||
|
||||
Devices can be organized by functional roles. These roles are fully cusomizable. For example, you might create roles for core switches, distribution switches, and access switches.
|
||||
|
||||
---
|
||||
|
||||
# Device Components
|
||||
|
||||
There are six types of device components which comprise all of the interconnection logic with NetBox:
|
||||
There are eight types of device components which comprise all of the interconnection logic with NetBox:
|
||||
|
||||
* Console ports
|
||||
* Console server ports
|
||||
* Power ports
|
||||
* Power outlets
|
||||
* Network interfaces
|
||||
* Front ports
|
||||
* Rear ports
|
||||
* Device bays
|
||||
|
||||
## Console
|
||||
### Console
|
||||
|
||||
Console ports connect only to console server ports. Console connections can be marked as either *planned* or *connected*.
|
||||
|
||||
## Power
|
||||
### Power
|
||||
|
||||
Power ports connect only to power outlets. Power connections can be marked as either *planned* or *connected*.
|
||||
|
||||
## Interfaces
|
||||
### Interfaces
|
||||
|
||||
Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. Each type of connection can be classified as either *planned* or *connected*.
|
||||
|
||||
@@ -91,17 +83,27 @@ Each interface can also be enabled or disabled, and optionally designated as man
|
||||
|
||||
VLANs can be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
|
||||
|
||||
## Device Bays
|
||||
### Pass-through Ports
|
||||
|
||||
Pass-through ports are used to model physical terminations which comprise part of a longer path, such as a cable terminated to a patch panel. Each front port maps to a position on a rear port. A 24-port UTP patch panel, for instance, would have 24 front ports and 24 rear ports. Although this relationship is typically one-to-one, a rear port may have multiple front ports mapped to it. This can be useful for modeling instances where multiple paths share a common cable (for example, six different fiber connections sharing a 12-strand MPO cable).
|
||||
|
||||
Pass-through ports can also be used to model "bump in the wire" devices, such as a media convertor or passive tap.
|
||||
|
||||
### Device Bays
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
# Platforms
|
||||
|
||||
A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
||||
A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of the same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
||||
|
||||
The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. See the [API documentation](api/napalm-integration.md) for more information on NAPALM integration.
|
||||
The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
|
||||
|
||||
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
||||
|
||||
@@ -118,3 +120,25 @@ Inventory items represent hardware components installed within a device, such as
|
||||
A virtual chassis represents a set of devices which share a single control plane: a stack of switches which are managed as a single device, for example. Each device in the virtual chassis is assigned a position and (optionally) a priority. Exactly one device is designated the virtual chassis master: This device will typically be assigned a name, secrets, services, and other attributes related to its management.
|
||||
|
||||
It's important to recognize the distinction between a virtual chassis and a chassis-based device. For instance, a virtual chassis is not used to model a chassis switch with removable line cards such as the Juniper EX9208, as its line cards are _not_ physically separate devices capable of operating independently.
|
||||
|
||||
---
|
||||
|
||||
# Cables
|
||||
|
||||
A cable represents a physical connection between two termination points, such as between a console port and a patch panel port, or between two network interfaces. Cables can be traced through pass-through ports to form a complete path between two endpoints. In the example below, three individual cables comprise a path between the two connected endpoints.
|
||||
|
||||
```
|
||||
|<------------------------------------------ Cable Path ------------------------------------------->|
|
||||
|
||||
Device A Patch Panel A Patch Panel B Device B
|
||||
+-----------+ +-------------+ +-------------+ +-----------+
|
||||
| Interface | --- Cable --- | Front Port | | Front Port | --- Cable --- | Interface |
|
||||
+-----------+ +-------------+ +-------------+ +-----------+
|
||||
+-------------+ +-------------+
|
||||
| Rear Port | --- Cable --- | Rear Port |
|
||||
+-------------+ +-------------+
|
||||
```
|
||||
|
||||
All connections between device components in NetBox are represented using cables. However, defining the actual cable plant is optional: Components can be be directly connected using cables with no type or other attributes assigned.
|
||||
|
||||
Cables are also used to associated ports and interfaces with circuit terminations. To do this, first create the circuit termination, then navigate the desired component and connect a cable between the two.
|
||||
|
||||
@@ -4,7 +4,7 @@ The first step to documenting your IP space is to define its scope by creating a
|
||||
|
||||
* 10.0.0.0/8 (RFC 1918)
|
||||
* 100.64.0.0/10 (RFC 6598)
|
||||
* 172.16.0.0/20 (RFC 1918)
|
||||
* 172.16.0.0/12 (RFC 1918)
|
||||
* 192.168.0.0/16 (RFC 1918)
|
||||
* One or more /48s within fd00::/8 (IPv6 unique local addressing)
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ Close the release milestone on GitHub. Ensure that there are no remaining open i
|
||||
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
||||
|
||||
## Update VERSION
|
||||
## Update Version and Changelog
|
||||
|
||||
Update the `VERSION` constant in `settings.py` to the new release.
|
||||
Update the `VERSION` constant in `settings.py` to the new release version and add the current date to the release notes in `CHANGELOG.md`.
|
||||
|
||||
## Submit a Pull Request
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ Checking connectivity... done.
|
||||
|
||||
`# chown -R netbox:netbox /opt/netbox/netbox/media/`
|
||||
|
||||
## Install Python Packages
|
||||
# Install Python Packages
|
||||
|
||||
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
|
||||
|
||||
@@ -82,7 +82,7 @@ Install the required Python packages using pip. (If you encounter any compilatio
|
||||
!!! note
|
||||
If you encounter errors while installing the required packages, check that you're running a recent version of pip (v9.0.1 or higher) with the command `pip3 -V`.
|
||||
|
||||
### NAPALM Automation (Optional)
|
||||
## NAPALM Automation (Optional)
|
||||
|
||||
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:
|
||||
|
||||
@@ -90,7 +90,7 @@ NetBox supports integration with the [NAPALM automation](https://napalm-automati
|
||||
# pip3 install napalm
|
||||
```
|
||||
|
||||
### Webhooks (Optional)
|
||||
## Webhooks (Optional)
|
||||
|
||||
[Webhooks](../data-model/extras/#webhooks) allow NetBox to integrate with external services by pushing out a notification each time a relevant object is created, updated, or deleted. Enabling the webhooks feature requires [Redis](https://redis.io/), a lightweight in-memory database. You may opt to install a Redis sevice locally (see below) or connect to an external one.
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
|
||||
## Option B: Apache
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y apache2
|
||||
# apt-get install -y apache2 libapache2-mod-wsgi-py3
|
||||
```
|
||||
|
||||
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
|
||||
|
||||
@@ -64,13 +64,6 @@ Once the new code is in place, run the upgrade script (which may need to be run
|
||||
# ./upgrade.sh
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below. Note that Python 2 will no longer be supported in NetBox v2.5.
|
||||
|
||||
```no-highlight
|
||||
# ./upgrade.sh -2
|
||||
```
|
||||
|
||||
This script:
|
||||
|
||||
* Installs or upgrades any new required Python packages
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
site_name: NetBox
|
||||
theme: readthedocs
|
||||
repo_url: https://github.com/digitalocean/netbox
|
||||
|
||||
pages:
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import serializers
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from circuits.constants import CIRCUIT_STATUS_CHOICES
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
||||
from dcim.api.serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
|
||||
@@ -87,10 +85,21 @@ class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
class CircuitTerminationSerializer(ValidatedModelSerializer):
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer()
|
||||
interface = InterfaceSerializer(required=False, allow_null=True)
|
||||
connected_endpoint = NestedInterfaceSerializer(read_only=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'id', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description', 'connected_endpoint', 'cable',
|
||||
]
|
||||
|
||||
|
||||
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'url', 'circuit', 'term_side']
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
@@ -17,7 +15,7 @@ router = routers.DefaultRouter()
|
||||
router.APIRootView = CircuitsRootView
|
||||
|
||||
# Field choices
|
||||
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, base_name='field-choice')
|
||||
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# Providers
|
||||
router.register(r'providers', views.ProviderViewSet)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
@@ -29,9 +27,9 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
#
|
||||
|
||||
class ProviderViewSet(CustomFieldModelViewSet):
|
||||
queryset = Provider.objects.all()
|
||||
queryset = Provider.objects.prefetch_related('tags')
|
||||
serializer_class = serializers.ProviderSerializer
|
||||
filter_class = filters.ProviderFilter
|
||||
filterset_class = filters.ProviderFilter
|
||||
|
||||
@action(detail=True)
|
||||
def graphs(self, request, pk=None):
|
||||
@@ -51,7 +49,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
||||
class CircuitTypeViewSet(ModelViewSet):
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = serializers.CircuitTypeSerializer
|
||||
filter_class = filters.CircuitTypeFilter
|
||||
filterset_class = filters.CircuitTypeFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -59,9 +57,9 @@ class CircuitTypeViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class CircuitViewSet(CustomFieldModelViewSet):
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider').prefetch_related('tags')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filter_class = filters.CircuitFilter
|
||||
filterset_class = filters.CircuitFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -69,6 +67,8 @@ class CircuitViewSet(CustomFieldModelViewSet):
|
||||
#
|
||||
|
||||
class CircuitTerminationViewSet(ModelViewSet):
|
||||
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
|
||||
queryset = CircuitTermination.objects.select_related(
|
||||
'circuit', 'site', 'connected_endpoint__device', 'cable'
|
||||
)
|
||||
serializer_class = serializers.CircuitTerminationSerializer
|
||||
filter_class = filters.CircuitTerminationFilter
|
||||
filterset_class = filters.CircuitTerminationFilter
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
# Circuit statuses
|
||||
CIRCUIT_STATUS_DEPROVISIONING = 0
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
@@ -12,24 +10,27 @@ from .models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
|
||||
|
||||
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='circuits__terminations__site',
|
||||
field_name='circuits__terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='circuits__terminations__site__slug',
|
||||
field_name='circuits__terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
name='tags__slug',
|
||||
field_name='tags__slug',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -56,7 +57,10 @@ class CircuitTypeFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -66,7 +70,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Provider (ID)',
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
name='provider__slug',
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Provider (slug)',
|
||||
@@ -76,7 +80,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Circuit type (ID)',
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
name='type__slug',
|
||||
field_name='type__slug',
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Circuit type (slug)',
|
||||
@@ -90,24 +94,24 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='terminations__site',
|
||||
field_name='terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='terminations__site__slug',
|
||||
field_name='terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
name='tags__slug',
|
||||
field_name='tags__slug',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -121,6 +125,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
Q(cid__icontains=value) |
|
||||
Q(terminations__xconnect_id__icontains=value) |
|
||||
Q(terminations__pp_info__icontains=value) |
|
||||
Q(terminations__description__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
@@ -140,7 +145,7 @@ class CircuitTerminationFilter(django_filters.FilterSet):
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@@ -156,5 +161,6 @@ class CircuitTerminationFilter(django_filters.FilterSet):
|
||||
return queryset.filter(
|
||||
Q(circuit__cid__icontains=value) |
|
||||
Q(xconnect_id__icontains=value) |
|
||||
Q(pp_info__icontains=value)
|
||||
Q(pp_info__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
).distinct()
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
from taggit.forms import TagField
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack
|
||||
from dcim.models import Site
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
AnnotatedMultipleChoiceField, APISelect, add_blank_choice, BootstrapMixin, ChainedFieldsMixin,
|
||||
ChainedModelChoiceField, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField,
|
||||
AnnotatedMultipleChoiceField, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField,
|
||||
SmallTextarea, SlugField,
|
||||
)
|
||||
from .constants import CIRCUIT_STATUS_CHOICES
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
@@ -205,59 +203,12 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack'}
|
||||
)
|
||||
)
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
('rack', 'rack'),
|
||||
),
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface'}
|
||||
)
|
||||
)
|
||||
interface = ChainedModelChoiceField(
|
||||
queryset=Interface.objects.connectable().select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
),
|
||||
chains=(
|
||||
('device', 'device'),
|
||||
),
|
||||
required=False,
|
||||
label='Interface',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical',
|
||||
disabled_indicator='is_connected'
|
||||
)
|
||||
)
|
||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info',
|
||||
'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
|
||||
]
|
||||
help_texts = {
|
||||
'port_speed': "Physical circuit speed",
|
||||
@@ -267,25 +218,3 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
|
||||
widgets = {
|
||||
'term_side': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
if instance and instance.interface is not None:
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
initial['rack'] = instance.interface.device.rack
|
||||
initial['device'] = instance.interface.device
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Mark connected interfaces as disabled
|
||||
self.fields['interface'].choices = []
|
||||
for iface in self.fields['interface'].queryset:
|
||||
self.fields['interface'].choices.append(
|
||||
(iface.id, {
|
||||
'label': iface.name,
|
||||
'disabled': iface.is_connected and iface.pk != self.initial.get('interface'),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-06-22 18:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:25
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-06-22 18:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-13 19:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-26 21:59
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-08 20:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-12-13 16:30
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-01-17 20:08
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-04-19 17:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-06 18:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.12 on 2018-05-22 19:04
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.12 on 2018-06-13 17:14
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
89
netbox/circuits/migrations/0013_cables.py
Normal file
89
netbox/circuits/migrations/0013_cables.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import sys
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||
|
||||
|
||||
def circuit_terminations_to_cables(apps, schema_editor):
|
||||
"""
|
||||
Copy all existing CircuitTermination Interface associations as Cables
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
|
||||
# Load content types
|
||||
circuittermination_type = ContentType.objects.get_for_model(CircuitTermination)
|
||||
interface_type = ContentType.objects.get_for_model(Interface)
|
||||
|
||||
# Create a new Cable instance from each console connection
|
||||
if 'test' not in sys.argv:
|
||||
print("\n Adding circuit terminations... ", end='', flush=True)
|
||||
for circuittermination in CircuitTermination.objects.filter(interface__isnull=False):
|
||||
|
||||
# Create the new Cable
|
||||
cable = Cable.objects.create(
|
||||
termination_a_type=circuittermination_type,
|
||||
termination_a_id=circuittermination.id,
|
||||
termination_b_type=interface_type,
|
||||
termination_b_id=circuittermination.interface_id,
|
||||
status=CONNECTION_STATUS_CONNECTED
|
||||
)
|
||||
|
||||
# Cache the Cable on its two termination points
|
||||
CircuitTermination.objects.filter(pk=circuittermination.pk).update(
|
||||
cable=cable,
|
||||
connected_endpoint=circuittermination.interface,
|
||||
connection_status=CONNECTION_STATUS_CONNECTED
|
||||
)
|
||||
# Cache the connected Cable on the Interface
|
||||
Interface.objects.filter(pk=circuittermination.interface_id).update(
|
||||
cable=cable,
|
||||
_connected_circuittermination=circuittermination,
|
||||
connection_status=CONNECTION_STATUS_CONNECTED
|
||||
)
|
||||
|
||||
cable_count = Cable.objects.filter(termination_a_type=circuittermination_type).count()
|
||||
if 'test' not in sys.argv:
|
||||
print("{} cables created".format(cable_count))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0012_change_logging'),
|
||||
('dcim', '0066_cables'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# Add new CircuitTermination fields
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='connected_endpoint',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='connection_status',
|
||||
field=models.NullBooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable'),
|
||||
),
|
||||
|
||||
# Copy CircuitTermination connections to Interfaces as Cables
|
||||
migrations.RunPython(circuit_terminations_to_cables),
|
||||
|
||||
# Remove interface field from CircuitTermination
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='interface',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.1.3 on 2018-11-05 18:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0013_cables'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -1,20 +1,17 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.constants import STATUS_CLASSES
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, STATUS_CLASSES
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import CableTermination
|
||||
from extras.models import CustomFieldModel, ObjectChange
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object
|
||||
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||
@@ -84,7 +81,6 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CircuitType(ChangeLoggedModel):
|
||||
"""
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
@@ -116,12 +112,11 @@ class CircuitType(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
|
||||
interface, but this is not required. Circuit port speed and commit rate are measured in Kbps.
|
||||
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
|
||||
in Kbps.
|
||||
"""
|
||||
cid = models.CharField(
|
||||
max_length=50,
|
||||
@@ -217,8 +212,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
return self._get_termination('Z')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CircuitTermination(models.Model):
|
||||
class CircuitTermination(CableTermination):
|
||||
circuit = models.ForeignKey(
|
||||
to='circuits.Circuit',
|
||||
on_delete=models.CASCADE,
|
||||
@@ -234,13 +228,17 @@ class CircuitTermination(models.Model):
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations'
|
||||
)
|
||||
interface = models.OneToOneField(
|
||||
connected_endpoint = models.OneToOneField(
|
||||
to='dcim.Interface',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_termination',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
default=CONNECTION_STATUS_CONNECTED
|
||||
)
|
||||
port_speed = models.PositiveIntegerField(
|
||||
verbose_name='Port speed (Kbps)'
|
||||
)
|
||||
@@ -260,13 +258,17 @@ class CircuitTermination(models.Model):
|
||||
blank=True,
|
||||
verbose_name='Patch panel/port(s)'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
unique_together = ['circuit', 'term_side']
|
||||
|
||||
def __str__(self):
|
||||
return '{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||
return 'Side {}'.format(self.get_term_side_display())
|
||||
|
||||
def log_change(self, user, request_id, action):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_tables2.utils import Accessor
|
||||
@@ -25,12 +23,6 @@ STATUS_LABEL = """
|
||||
class CircuitTerminationColumn(tables.Column):
|
||||
|
||||
def render(self, value):
|
||||
if value.interface:
|
||||
return mark_safe('<a href="{}" title="{}">{}</a>'.format(
|
||||
value.interface.device.get_absolute_url(),
|
||||
value.site,
|
||||
value.interface.device
|
||||
))
|
||||
return mark_safe('<a href="{}">{}</a>'.format(
|
||||
value.site.get_absolute_url(),
|
||||
value.site
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.models import Site
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
|
||||
from extras.constants import GRAPH_TYPE_PROVIDER
|
||||
from extras.models import Graph
|
||||
from utilities.testing import APITestCase
|
||||
@@ -56,6 +54,16 @@ class ProviderTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_providers_brief(self):
|
||||
|
||||
url = reverse('circuits-api:provider-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_provider(self):
|
||||
|
||||
data = {
|
||||
@@ -147,6 +155,16 @@ class CircuitTypeTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_circuittypes_brief(self):
|
||||
|
||||
url = reverse('circuits-api:circuittype-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_circuittype(self):
|
||||
|
||||
data = {
|
||||
@@ -216,6 +234,16 @@ class CircuitTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_circuits_brief(self):
|
||||
|
||||
url = reverse('circuits-api:circuit-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['cid', 'id', 'url']
|
||||
)
|
||||
|
||||
def test_create_circuit(self):
|
||||
|
||||
data = {
|
||||
@@ -300,21 +328,24 @@ class CircuitTerminationTest(APITestCase):
|
||||
|
||||
super(CircuitTerminationTest, self).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')
|
||||
provider = Provider.objects.create(name='Test Provider', slug='test-provider')
|
||||
circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type')
|
||||
self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=provider, type=circuittype)
|
||||
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
|
||||
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
|
||||
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')
|
||||
self.circuittermination1 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
)
|
||||
self.circuittermination2 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
|
||||
)
|
||||
self.circuittermination3 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
)
|
||||
self.circuittermination4 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
|
||||
)
|
||||
|
||||
def test_get_circuittermination(self):
|
||||
@@ -329,14 +360,14 @@ class CircuitTerminationTest(APITestCase):
|
||||
url = reverse('circuits-api:circuittermination-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
self.assertEqual(response.data['count'], 4)
|
||||
|
||||
def test_create_circuittermination(self):
|
||||
|
||||
data = {
|
||||
'circuit': self.circuit1.pk,
|
||||
'term_side': TERM_SIDE_Z,
|
||||
'site': self.site2.pk,
|
||||
'circuit': self.circuit3.pk,
|
||||
'term_side': TERM_SIDE_A,
|
||||
'site': self.site1.pk,
|
||||
'port_speed': 1000000,
|
||||
}
|
||||
|
||||
@@ -344,7 +375,7 @@ class CircuitTerminationTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(CircuitTermination.objects.count(), 4)
|
||||
self.assertEqual(CircuitTermination.objects.count(), 5)
|
||||
circuittermination4 = CircuitTermination.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(circuittermination4.circuit_id, data['circuit'])
|
||||
self.assertEqual(circuittermination4.term_side, data['term_side'])
|
||||
@@ -353,20 +384,23 @@ class CircuitTerminationTest(APITestCase):
|
||||
|
||||
def test_update_circuittermination(self):
|
||||
|
||||
circuittermination5 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||
)
|
||||
|
||||
data = {
|
||||
'circuit': self.circuit1.pk,
|
||||
'circuit': self.circuit3.pk,
|
||||
'term_side': TERM_SIDE_Z,
|
||||
'site': self.site2.pk,
|
||||
'port_speed': 1000000,
|
||||
}
|
||||
|
||||
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
|
||||
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': circuittermination5.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(CircuitTermination.objects.count(), 3)
|
||||
self.assertEqual(CircuitTermination.objects.count(), 5)
|
||||
circuittermination1 = CircuitTermination.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(circuittermination1.circuit_id, data['circuit'])
|
||||
self.assertEqual(circuittermination1.term_side, data['term_side'])
|
||||
self.assertEqual(circuittermination1.site_id, data['site'])
|
||||
self.assertEqual(circuittermination1.port_speed, data['port_speed'])
|
||||
@@ -377,4 +411,4 @@ class CircuitTerminationTest(APITestCase):
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(CircuitTermination.objects.count(), 2)
|
||||
self.assertEqual(CircuitTermination.objects.count(), 3)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from dcim.views import CableCreateView, CableTraceView
|
||||
from extras.views import ObjectChangeLogView
|
||||
from . import views
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
@@ -44,5 +43,7 @@ urlpatterns = [
|
||||
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||
url(r'^circuit-terminations/(?P<termination_a_id>\d+)/connect/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/trace/$', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
|
||||
|
||||
]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
@@ -134,7 +132,7 @@ class CircuitListView(ObjectListView):
|
||||
queryset = Circuit.objects.select_related(
|
||||
'provider', 'type', 'tenant'
|
||||
).prefetch_related(
|
||||
'terminations__site', 'terminations__interface__device'
|
||||
'terminations__site'
|
||||
)
|
||||
filter = filters.CircuitFilter
|
||||
filter_form = forms.CircuitFilterForm
|
||||
@@ -148,12 +146,12 @@ class CircuitView(View):
|
||||
|
||||
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
|
||||
termination_a = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
'site__region', 'connected_endpoint__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_A
|
||||
).first()
|
||||
termination_z = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
'site__region', 'connected_endpoint__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_Z
|
||||
).first()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from dcim.constants import (
|
||||
CONNECTION_STATUS_CHOICES, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_MODE_CHOICES, IFACE_ORDERING_CHOICES,
|
||||
RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||
)
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from utilities.api import (
|
||||
ChoiceField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
|
||||
WritableNestedSerializer,
|
||||
ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
|
||||
WritableNestedSerializer, get_serializer_for_model,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
|
||||
@@ -120,18 +115,21 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
|
||||
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True)
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False)
|
||||
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
|
||||
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
|
||||
outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
|
||||
'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial',
|
||||
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
|
||||
# prevents facility_id from being interpreted as a required field.
|
||||
@@ -222,17 +220,15 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
|
||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False)
|
||||
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False)
|
||||
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
|
||||
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'instance_count',
|
||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -306,6 +302,49 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
#
|
||||
# Rear port templates
|
||||
#
|
||||
|
||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'positions']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Front port templates
|
||||
#
|
||||
|
||||
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
rear_port = NestedRearPortTemplateSerializer()
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position']
|
||||
|
||||
|
||||
class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Device bay templates
|
||||
#
|
||||
@@ -346,7 +385,7 @@ class PlatformSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'rpc_client']
|
||||
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
|
||||
|
||||
|
||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
@@ -362,7 +401,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
||||
class DeviceIPAddressSerializer(serializers.ModelSerializer):
|
||||
class DeviceIPAddressSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
|
||||
class Meta:
|
||||
@@ -371,7 +410,7 @@ class DeviceIPAddressSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
|
||||
class NestedClusterSerializer(serializers.ModelSerializer):
|
||||
class NestedClusterSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||
|
||||
class Meta:
|
||||
@@ -380,7 +419,7 @@ class NestedClusterSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
# Cannot import NestedVirtualChassisSerializer due to circular dependency
|
||||
class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
|
||||
class DeviceVirtualChassisSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer()
|
||||
|
||||
@@ -396,7 +435,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer()
|
||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False)
|
||||
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||
@@ -412,7 +451,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
'last_updated', 'local_context_data',
|
||||
]
|
||||
validators = []
|
||||
|
||||
@@ -448,25 +487,88 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
|
||||
'config_context', 'created', 'last_updated',
|
||||
'config_context', 'created', 'last_updated', 'local_context_data',
|
||||
]
|
||||
|
||||
def get_config_context(self, obj):
|
||||
return obj.get_config_context()
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
||||
class CableSerializer(ValidatedModelSerializer):
|
||||
termination_a_type = ContentTypeField()
|
||||
termination_b_type = ContentTypeField()
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'id', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'termination_b_id',
|
||||
'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit',
|
||||
]
|
||||
|
||||
def _get_termination(self, obj, side):
|
||||
"""
|
||||
Serialize a nested representation of a termination.
|
||||
"""
|
||||
if side.lower() not in ['a', 'b']:
|
||||
raise ValueError("Termination side must be either A or B.")
|
||||
termination = getattr(obj, 'termination_{}'.format(side.lower()))
|
||||
if termination is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(termination, prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
data = serializer(termination, context=context).data
|
||||
|
||||
return data
|
||||
|
||||
def get_termination_a(self, obj):
|
||||
return self._get_termination(obj, 'a')
|
||||
|
||||
def get_termination_b(self, obj):
|
||||
return self._get_termination(obj, 'b')
|
||||
|
||||
|
||||
class TracedCableSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Used only while tracing a cable path.
|
||||
"""
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit',
|
||||
]
|
||||
|
||||
|
||||
class NestedCableSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'url', 'label']
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'device', 'name', 'connected_console', 'tags']
|
||||
read_only_fields = ['connected_console']
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'cable', 'tags']
|
||||
read_only_fields = ['connected_endpoint']
|
||||
|
||||
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
@@ -475,7 +577,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
@@ -484,12 +586,22 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cs_port = NestedConsoleServerPortSerializer(required=False, allow_null=True)
|
||||
connected_endpoint = NestedConsoleServerPortSerializer(read_only=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status', 'tags']
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
@@ -498,12 +610,13 @@ class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
|
||||
class PowerOutletSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'device', 'name', 'connected_port', 'tags']
|
||||
read_only_fields = ['connected_port']
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'cable', 'tags']
|
||||
read_only_fields = ['connected_endpoint']
|
||||
|
||||
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
@@ -512,7 +625,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
@@ -521,12 +634,22 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
|
||||
class PowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
power_outlet = NestedPowerOutletSerializer(required=False, allow_null=True)
|
||||
connected_endpoint = NestedPowerOutletSerializer(read_only=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status', 'tags']
|
||||
fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
@@ -539,7 +662,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
||||
@@ -573,10 +696,8 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||
interface_connection = serializers.SerializerMethodField(read_only=True)
|
||||
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False)
|
||||
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@@ -584,14 +705,14 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||
'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans',
|
||||
'tags',
|
||||
'connected_endpoint', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@@ -613,27 +734,75 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
|
||||
return super(InterfaceSerializer, self).validate(data)
|
||||
|
||||
def get_is_connected(self, obj):
|
||||
def get_connected_endpoint(self, obj):
|
||||
"""
|
||||
Return True if the interface has a connected interface or circuit termination.
|
||||
Return the appropriate serializer for the type of connected object.
|
||||
"""
|
||||
if obj.connection:
|
||||
return True
|
||||
try:
|
||||
circuit_termination = obj.circuit_termination
|
||||
return True
|
||||
except CircuitTermination.DoesNotExist:
|
||||
pass
|
||||
return False
|
||||
if obj.connected_endpoint is None:
|
||||
return None
|
||||
|
||||
def get_interface_connection(self, obj):
|
||||
if obj.connection:
|
||||
context = {
|
||||
'request': self.context['request'],
|
||||
'interface': obj.connected_interface,
|
||||
}
|
||||
return ContextualInterfaceConnectionSerializer(obj.connection, context=context).data
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
data = serializer(obj.connected_endpoint, context=context).data
|
||||
|
||||
return data
|
||||
|
||||
|
||||
#
|
||||
# Rear ports
|
||||
#
|
||||
|
||||
class RearPortSerializer(ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'device', 'name', 'type', 'positions', 'cable', 'tags']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
# Front ports
|
||||
#
|
||||
|
||||
class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class FrontPortSerializer(ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PORT_TYPE_CHOICES)
|
||||
rear_port = FrontPortRearPortSerializer()
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'cable', 'tags']
|
||||
|
||||
|
||||
class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
#
|
||||
@@ -651,11 +820,12 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
@@ -666,7 +836,7 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
# Provide a default value to satisfy UniqueTogetherValidator
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -682,36 +852,17 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
#
|
||||
|
||||
class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
interface_a = NestedInterfaceSerializer()
|
||||
interface_b = NestedInterfaceSerializer()
|
||||
interface_a = serializers.SerializerMethodField()
|
||||
interface_b = NestedInterfaceSerializer(source='connected_endpoint')
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
||||
model = Interface
|
||||
fields = ['interface_a', 'interface_b', 'connection_status']
|
||||
|
||||
|
||||
class NestedInterfaceConnectionSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['id', 'url', 'connection_status']
|
||||
|
||||
|
||||
class ContextualInterfaceConnectionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
A read-only representation of an InterfaceConnection from the perspective of either of its two connected Interfaces.
|
||||
"""
|
||||
interface = serializers.SerializerMethodField(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['id', 'interface', 'connection_status']
|
||||
|
||||
def get_interface(self, obj):
|
||||
return NestedInterfaceSerializer(self.context['interface'], context=self.context).data
|
||||
def get_interface_a(self, obj):
|
||||
context = {'request': self.context['request']}
|
||||
return NestedInterfaceSerializer(instance=obj, context=context).data
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
@@ -17,7 +15,7 @@ router = routers.DefaultRouter()
|
||||
router.APIRootView = DCIMRootView
|
||||
|
||||
# Field choices
|
||||
router.register(r'_choices', views.DCIMFieldChoicesViewSet, base_name='field-choice')
|
||||
router.register(r'_choices', views.DCIMFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# Sites
|
||||
router.register(r'regions', views.RegionViewSet)
|
||||
@@ -39,6 +37,8 @@ router.register(r'console-server-port-templates', views.ConsoleServerPortTemplat
|
||||
router.register(r'power-port-templates', views.PowerPortTemplateViewSet)
|
||||
router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet)
|
||||
router.register(r'interface-templates', views.InterfaceTemplateViewSet)
|
||||
router.register(r'front-port-templates', views.FrontPortTemplateViewSet)
|
||||
router.register(r'rear-port-templates', views.RearPortTemplateViewSet)
|
||||
router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet)
|
||||
|
||||
# Devices
|
||||
@@ -52,19 +52,24 @@ router.register(r'console-server-ports', views.ConsoleServerPortViewSet)
|
||||
router.register(r'power-ports', views.PowerPortViewSet)
|
||||
router.register(r'power-outlets', views.PowerOutletViewSet)
|
||||
router.register(r'interfaces', views.InterfaceViewSet)
|
||||
router.register(r'front-ports', views.FrontPortViewSet)
|
||||
router.register(r'rear-ports', views.RearPortViewSet)
|
||||
router.register(r'device-bays', views.DeviceBayViewSet)
|
||||
router.register(r'inventory-items', views.InventoryItemViewSet)
|
||||
|
||||
# Connections
|
||||
router.register(r'console-connections', views.ConsoleConnectionViewSet, base_name='consoleconnections')
|
||||
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
|
||||
router.register(r'interface-connections', views.InterfaceConnectionViewSet)
|
||||
router.register(r'console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections')
|
||||
router.register(r'power-connections', views.PowerConnectionViewSet, basename='powerconnections')
|
||||
router.register(r'interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections')
|
||||
|
||||
# Cables
|
||||
router.register(r'cables', views.CableViewSet)
|
||||
|
||||
# Virtual chassis
|
||||
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
|
||||
|
||||
# Miscellaneous
|
||||
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
|
||||
router.register(r'connected-device', views.ConnectedDeviceViewSet, basename='connected-device')
|
||||
|
||||
app_name = 'dcim-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.db.models import F, Q
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.openapi import Parameter
|
||||
@@ -15,15 +14,17 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||
|
||||
from dcim import filters
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable
|
||||
from utilities.api import (
|
||||
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
|
||||
)
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
|
||||
@@ -36,8 +37,7 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Device, ['face', 'status']),
|
||||
(ConsolePort, ['connection_status']),
|
||||
(Interface, ['form_factor', 'mode']),
|
||||
(InterfaceConnection, ['connection_status']),
|
||||
(Interface, ['connection_status', 'form_factor', 'mode']),
|
||||
(InterfaceTemplate, ['form_factor']),
|
||||
(PowerPort, ['connection_status']),
|
||||
(Rack, ['type', 'width']),
|
||||
@@ -45,6 +45,40 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
)
|
||||
|
||||
|
||||
# Mixins
|
||||
|
||||
class CableTraceMixin(object):
|
||||
|
||||
@action(detail=True, url_path='trace')
|
||||
def trace(self, request, pk):
|
||||
"""
|
||||
Trace a complete cable path and return each segment as a three-tuple of (termination, cable, termination).
|
||||
"""
|
||||
obj = get_object_or_404(self.queryset.model, pk=pk)
|
||||
|
||||
# Initialize the path array
|
||||
path = []
|
||||
|
||||
for near_end, cable, far_end in obj.trace():
|
||||
|
||||
# Serialize each object
|
||||
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
|
||||
x = serializer_a(near_end, context={'request': request}).data
|
||||
if cable is not None:
|
||||
y = serializers.TracedCableSerializer(cable, context={'request': request}).data
|
||||
else:
|
||||
y = None
|
||||
if far_end is not None:
|
||||
serializer_b = get_serializer_for_model(far_end, prefix='Nested')
|
||||
z = serializer_b(far_end, context={'request': request}).data
|
||||
else:
|
||||
z = None
|
||||
|
||||
path.append((x, y, z))
|
||||
|
||||
return Response(path)
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
@@ -52,7 +86,7 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
class RegionViewSet(ModelViewSet):
|
||||
queryset = Region.objects.all()
|
||||
serializer_class = serializers.RegionSerializer
|
||||
filter_class = filters.RegionFilter
|
||||
filterset_class = filters.RegionFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -60,9 +94,9 @@ class RegionViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class SiteViewSet(CustomFieldModelViewSet):
|
||||
queryset = Site.objects.select_related('region', 'tenant')
|
||||
queryset = Site.objects.select_related('region', 'tenant').prefetch_related('tags')
|
||||
serializer_class = serializers.SiteSerializer
|
||||
filter_class = filters.SiteFilter
|
||||
filterset_class = filters.SiteFilter
|
||||
|
||||
@action(detail=True)
|
||||
def graphs(self, request, pk=None):
|
||||
@@ -82,7 +116,7 @@ class SiteViewSet(CustomFieldModelViewSet):
|
||||
class RackGroupViewSet(ModelViewSet):
|
||||
queryset = RackGroup.objects.select_related('site')
|
||||
serializer_class = serializers.RackGroupSerializer
|
||||
filter_class = filters.RackGroupFilter
|
||||
filterset_class = filters.RackGroupFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -92,7 +126,7 @@ class RackGroupViewSet(ModelViewSet):
|
||||
class RackRoleViewSet(ModelViewSet):
|
||||
queryset = RackRole.objects.all()
|
||||
serializer_class = serializers.RackRoleSerializer
|
||||
filter_class = filters.RackRoleFilter
|
||||
filterset_class = filters.RackRoleFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -100,9 +134,9 @@ class RackRoleViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class RackViewSet(CustomFieldModelViewSet):
|
||||
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
|
||||
queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('tags')
|
||||
serializer_class = serializers.RackSerializer
|
||||
filter_class = filters.RackFilter
|
||||
filterset_class = filters.RackFilter
|
||||
|
||||
@action(detail=True)
|
||||
def units(self, request, pk=None):
|
||||
@@ -132,7 +166,7 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
class RackReservationViewSet(ModelViewSet):
|
||||
queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
|
||||
serializer_class = serializers.RackReservationSerializer
|
||||
filter_class = filters.RackReservationFilter
|
||||
filterset_class = filters.RackReservationFilter
|
||||
|
||||
# Assign user from request
|
||||
def perform_create(self, serializer):
|
||||
@@ -146,7 +180,7 @@ class RackReservationViewSet(ModelViewSet):
|
||||
class ManufacturerViewSet(ModelViewSet):
|
||||
queryset = Manufacturer.objects.all()
|
||||
serializer_class = serializers.ManufacturerSerializer
|
||||
filter_class = filters.ManufacturerFilter
|
||||
filterset_class = filters.ManufacturerFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -154,9 +188,9 @@ class ManufacturerViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags')
|
||||
serializer_class = serializers.DeviceTypeSerializer
|
||||
filter_class = filters.DeviceTypeFilter
|
||||
filterset_class = filters.DeviceTypeFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -166,37 +200,49 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||
class ConsolePortTemplateViewSet(ModelViewSet):
|
||||
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.ConsolePortTemplateSerializer
|
||||
filter_class = filters.ConsolePortTemplateFilter
|
||||
filterset_class = filters.ConsolePortTemplateFilter
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateViewSet(ModelViewSet):
|
||||
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
||||
filter_class = filters.ConsoleServerPortTemplateFilter
|
||||
filterset_class = filters.ConsoleServerPortTemplateFilter
|
||||
|
||||
|
||||
class PowerPortTemplateViewSet(ModelViewSet):
|
||||
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.PowerPortTemplateSerializer
|
||||
filter_class = filters.PowerPortTemplateFilter
|
||||
filterset_class = filters.PowerPortTemplateFilter
|
||||
|
||||
|
||||
class PowerOutletTemplateViewSet(ModelViewSet):
|
||||
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.PowerOutletTemplateSerializer
|
||||
filter_class = filters.PowerOutletTemplateFilter
|
||||
filterset_class = filters.PowerOutletTemplateFilter
|
||||
|
||||
|
||||
class InterfaceTemplateViewSet(ModelViewSet):
|
||||
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.InterfaceTemplateSerializer
|
||||
filter_class = filters.InterfaceTemplateFilter
|
||||
filterset_class = filters.InterfaceTemplateFilter
|
||||
|
||||
|
||||
class FrontPortTemplateViewSet(ModelViewSet):
|
||||
queryset = FrontPortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.FrontPortTemplateSerializer
|
||||
filterset_class = filters.FrontPortTemplateFilter
|
||||
|
||||
|
||||
class RearPortTemplateViewSet(ModelViewSet):
|
||||
queryset = RearPortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.RearPortTemplateSerializer
|
||||
filterset_class = filters.RearPortTemplateFilter
|
||||
|
||||
|
||||
class DeviceBayTemplateViewSet(ModelViewSet):
|
||||
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||
filter_class = filters.DeviceBayTemplateFilter
|
||||
filterset_class = filters.DeviceBayTemplateFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -206,7 +252,7 @@ class DeviceBayTemplateViewSet(ModelViewSet):
|
||||
class DeviceRoleViewSet(ModelViewSet):
|
||||
queryset = DeviceRole.objects.all()
|
||||
serializer_class = serializers.DeviceRoleSerializer
|
||||
filter_class = filters.DeviceRoleFilter
|
||||
filterset_class = filters.DeviceRoleFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -216,7 +262,7 @@ class DeviceRoleViewSet(ModelViewSet):
|
||||
class PlatformViewSet(ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filter_class = filters.PlatformFilter
|
||||
filterset_class = filters.PlatformFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -228,9 +274,9 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
||||
'virtual_chassis__master',
|
||||
).prefetch_related(
|
||||
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
|
||||
'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
||||
)
|
||||
filter_class = filters.DeviceFilter
|
||||
filterset_class = filters.DeviceFilter
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
@@ -238,6 +284,11 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
if self.action == 'retrieve':
|
||||
return serializers.DeviceWithConfigContextSerializer
|
||||
|
||||
request = self.get_serializer_context()['request']
|
||||
if request.query_params.get('brief', False):
|
||||
return serializers.NestedDeviceSerializer
|
||||
|
||||
return serializers.DeviceSerializer
|
||||
|
||||
@action(detail=True, url_path='napalm')
|
||||
@@ -314,34 +365,54 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortViewSet(ModelViewSet):
|
||||
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
|
||||
class ConsolePortViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = ConsolePort.objects.select_related(
|
||||
'device', 'connected_endpoint__device', 'cable'
|
||||
).prefetch_related(
|
||||
'tags'
|
||||
)
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filter_class = filters.ConsolePortFilter
|
||||
filterset_class = filters.ConsolePortFilter
|
||||
|
||||
|
||||
class ConsoleServerPortViewSet(ModelViewSet):
|
||||
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
|
||||
class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = ConsoleServerPort.objects.select_related(
|
||||
'device', 'connected_endpoint__device', 'cable'
|
||||
).prefetch_related(
|
||||
'tags'
|
||||
)
|
||||
serializer_class = serializers.ConsoleServerPortSerializer
|
||||
filter_class = filters.ConsoleServerPortFilter
|
||||
filterset_class = filters.ConsoleServerPortFilter
|
||||
|
||||
|
||||
class PowerPortViewSet(ModelViewSet):
|
||||
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
|
||||
class PowerPortViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = PowerPort.objects.select_related(
|
||||
'device', 'connected_endpoint__device', 'cable'
|
||||
).prefetch_related(
|
||||
'tags'
|
||||
)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filter_class = filters.PowerPortFilter
|
||||
filterset_class = filters.PowerPortFilter
|
||||
|
||||
|
||||
class PowerOutletViewSet(ModelViewSet):
|
||||
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
|
||||
class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = PowerOutlet.objects.select_related(
|
||||
'device', 'connected_endpoint__device', 'cable'
|
||||
).prefetch_related(
|
||||
'tags'
|
||||
)
|
||||
serializer_class = serializers.PowerOutletSerializer
|
||||
filter_class = filters.PowerOutletFilter
|
||||
filterset_class = filters.PowerOutletFilter
|
||||
|
||||
|
||||
class InterfaceViewSet(ModelViewSet):
|
||||
queryset = Interface.objects.select_related('device')
|
||||
class InterfaceViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = Interface.objects.select_related(
|
||||
'device', '_connected_interface', '_connected_circuittermination', 'cable'
|
||||
).prefetch_related(
|
||||
'ip_addresses', 'tags'
|
||||
)
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filter_class = filters.InterfaceFilter
|
||||
filterset_class = filters.InterfaceFilter
|
||||
|
||||
@action(detail=True)
|
||||
def graphs(self, request, pk=None):
|
||||
@@ -354,16 +425,36 @@ class InterfaceViewSet(ModelViewSet):
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class FrontPortViewSet(ModelViewSet):
|
||||
queryset = FrontPort.objects.select_related(
|
||||
'device__device_type__manufacturer', 'rear_port', 'cable'
|
||||
).prefetch_related(
|
||||
'tags'
|
||||
)
|
||||
serializer_class = serializers.FrontPortSerializer
|
||||
filterset_class = filters.FrontPortFilter
|
||||
|
||||
|
||||
class RearPortViewSet(ModelViewSet):
|
||||
queryset = RearPort.objects.select_related(
|
||||
'device__device_type__manufacturer', 'cable'
|
||||
).prefetch_related(
|
||||
'tags'
|
||||
)
|
||||
serializer_class = serializers.RearPortSerializer
|
||||
filterset_class = filters.RearPortFilter
|
||||
|
||||
|
||||
class DeviceBayViewSet(ModelViewSet):
|
||||
queryset = DeviceBay.objects.select_related('installed_device')
|
||||
queryset = DeviceBay.objects.select_related('installed_device').prefetch_related('tags')
|
||||
serializer_class = serializers.DeviceBaySerializer
|
||||
filter_class = filters.DeviceBayFilter
|
||||
filterset_class = filters.DeviceBayFilter
|
||||
|
||||
|
||||
class InventoryItemViewSet(ModelViewSet):
|
||||
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||
queryset = InventoryItem.objects.select_related('device', 'manufacturer').prefetch_related('tags')
|
||||
serializer_class = serializers.InventoryItemSerializer
|
||||
filter_class = filters.InventoryItemFilter
|
||||
filterset_class = filters.InventoryItemFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -371,21 +462,47 @@ class InventoryItemViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = ConsolePort.objects.select_related('device', 'cs_port__device').filter(cs_port__isnull=False)
|
||||
queryset = ConsolePort.objects.select_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
)
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filter_class = filters.ConsoleConnectionFilter
|
||||
filterset_class = filters.ConsoleConnectionFilter
|
||||
|
||||
|
||||
class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = PowerPort.objects.select_related('device', 'power_outlet__device').filter(power_outlet__isnull=False)
|
||||
queryset = PowerPort.objects.select_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filter_class = filters.PowerConnectionFilter
|
||||
filterset_class = filters.PowerConnectionFilter
|
||||
|
||||
|
||||
class InterfaceConnectionViewSet(ModelViewSet):
|
||||
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
|
||||
queryset = Interface.objects.select_related(
|
||||
'device', '_connected_interface', '_connected_circuittermination'
|
||||
).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)
|
||||
)
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
filter_class = filters.InterfaceConnectionFilter
|
||||
filterset_class = filters.InterfaceConnectionFilter
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
||||
class CableViewSet(ModelViewSet):
|
||||
queryset = Cable.objects.prefetch_related(
|
||||
'termination_a', 'termination_b'
|
||||
)
|
||||
serializer_class = serializers.CableSerializer
|
||||
filterset_class = filters.CableFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -393,7 +510,7 @@ class InterfaceConnectionViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class VirtualChassisViewSet(ModelViewSet):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
queryset = VirtualChassis.objects.prefetch_related('tags')
|
||||
serializer_class = serializers.VirtualChassisSerializer
|
||||
|
||||
|
||||
@@ -407,30 +524,43 @@ class ConnectedDeviceViewSet(ViewSet):
|
||||
interface. This is useful in a situation where a device boots with no configuration, but can detect its neighbors
|
||||
via a protocol such as LLDP. Two query parameters must be included in the request:
|
||||
|
||||
* `peer-device`: The name of the peer device
|
||||
* `peer-interface`: The name of the peer interface
|
||||
* `peer_device`: The name of the peer device
|
||||
* `peer_interface`: The name of the peer interface
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
_device_param = Parameter('peer-device', 'query',
|
||||
description='The name of the peer device', required=True, type=openapi.TYPE_STRING)
|
||||
_interface_param = Parameter('peer-interface', 'query',
|
||||
description='The name of the peer interface', required=True, type=openapi.TYPE_STRING)
|
||||
_device_param = Parameter(
|
||||
name='peer_device',
|
||||
in_='query',
|
||||
description='The name of the peer device',
|
||||
required=True,
|
||||
type=openapi.TYPE_STRING
|
||||
)
|
||||
_interface_param = Parameter(
|
||||
name='peer_interface',
|
||||
in_='query',
|
||||
description='The name of the peer interface',
|
||||
required=True,
|
||||
type=openapi.TYPE_STRING
|
||||
)
|
||||
|
||||
def get_view_name(self):
|
||||
return "Connected Device Locator"
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[_device_param, _interface_param], responses={'200': serializers.DeviceSerializer})
|
||||
manual_parameters=[_device_param, _interface_param],
|
||||
responses={'200': serializers.DeviceSerializer}
|
||||
)
|
||||
def list(self, request):
|
||||
|
||||
peer_device_name = request.query_params.get(self._device_param.name)
|
||||
peer_interface_name = request.query_params.get(self._interface_param.name)
|
||||
|
||||
if not peer_device_name or not peer_interface_name:
|
||||
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')
|
||||
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
|
||||
|
||||
# Determine local interface from peer interface's connection
|
||||
peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)
|
||||
local_interface = peer_interface.connected_interface
|
||||
local_interface = peer_interface._connected_interface
|
||||
|
||||
if local_interface is None:
|
||||
return Response()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
# Rack types
|
||||
RACK_TYPE_2POST = 100
|
||||
@@ -31,6 +29,20 @@ RACK_FACE_CHOICES = [
|
||||
[RACK_FACE_REAR, 'Rear'],
|
||||
]
|
||||
|
||||
# Rack statuses
|
||||
RACK_STATUS_RESERVED = 0
|
||||
RACK_STATUS_AVAILABLE = 1
|
||||
RACK_STATUS_PLANNED = 2
|
||||
RACK_STATUS_ACTIVE = 3
|
||||
RACK_STATUS_DEPRECATED = 4
|
||||
RACK_STATUS_CHOICES = [
|
||||
[RACK_STATUS_ACTIVE, 'Active'],
|
||||
[RACK_STATUS_PLANNED, 'Planned'],
|
||||
[RACK_STATUS_RESERVED, 'Reserved'],
|
||||
[RACK_STATUS_AVAILABLE, 'Available'],
|
||||
[RACK_STATUS_DEPRECATED, 'Deprecated'],
|
||||
]
|
||||
|
||||
# Parent/child device roles
|
||||
SUBDEVICE_ROLE_PARENT = True
|
||||
SUBDEVICE_ROLE_CHILD = False
|
||||
@@ -93,6 +105,11 @@ IFACE_FF_STACKWISE_PLUS = 5050
|
||||
IFACE_FF_FLEXSTACK = 5100
|
||||
IFACE_FF_FLEXSTACK_PLUS = 5150
|
||||
IFACE_FF_JUNIPER_VCP = 5200
|
||||
IFACE_FF_SUMMITSTACK = 5300
|
||||
IFACE_FF_SUMMITSTACK128 = 5310
|
||||
IFACE_FF_SUMMITSTACK256 = 5320
|
||||
IFACE_FF_SUMMITSTACK512 = 5330
|
||||
|
||||
# Other
|
||||
IFACE_FF_OTHER = 32767
|
||||
|
||||
@@ -168,6 +185,10 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
||||
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
|
||||
[IFACE_FF_SUMMITSTACK, 'Extreme SummitStack'],
|
||||
[IFACE_FF_SUMMITSTACK128, 'Extreme SummitStack-128'],
|
||||
[IFACE_FF_SUMMITSTACK256, 'Extreme SummitStack-256'],
|
||||
[IFACE_FF_SUMMITSTACK512, 'Extreme SummitStack-512'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -202,6 +223,36 @@ IFACE_MODE_CHOICES = [
|
||||
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
|
||||
]
|
||||
|
||||
# Pass-through port types
|
||||
PORT_TYPE_8P8C = 1000
|
||||
PORT_TYPE_ST = 2000
|
||||
PORT_TYPE_SC_SIMPLEX = 2100
|
||||
PORT_TYPE_SC_DUPLEX = 2110
|
||||
PORT_TYPE_FC = 2200
|
||||
PORT_TYPE_LC = 2300
|
||||
PORT_TYPE_MTRJ = 2400
|
||||
PORT_TYPE_MPO = 2500
|
||||
PORT_TYPE_CHOICES = [
|
||||
[
|
||||
'Copper',
|
||||
[
|
||||
[PORT_TYPE_8P8C, '8P8C'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'Fiber Optic',
|
||||
[
|
||||
[PORT_TYPE_ST, 'ST'],
|
||||
[PORT_TYPE_SC_SIMPLEX, 'SC (Simplex)'],
|
||||
[PORT_TYPE_SC_DUPLEX, 'SC (Duplex)'],
|
||||
[PORT_TYPE_FC, 'FC'],
|
||||
[PORT_TYPE_LC, 'LC'],
|
||||
[PORT_TYPE_MTRJ, 'MTRJ'],
|
||||
[PORT_TYPE_MPO, 'MPO'],
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
# Device statuses
|
||||
DEVICE_STATUS_OFFLINE = 0
|
||||
DEVICE_STATUS_ACTIVE = 1
|
||||
@@ -228,7 +279,7 @@ SITE_STATUS_CHOICES = [
|
||||
[SITE_STATUS_RETIRED, 'Retired'],
|
||||
]
|
||||
|
||||
# Bootstrap CSS classes for device statuses
|
||||
# Bootstrap CSS classes for device/rack statuses
|
||||
STATUS_CLASSES = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
@@ -246,12 +297,81 @@ CONNECTION_STATUS_CHOICES = [
|
||||
[CONNECTION_STATUS_CONNECTED, 'Connected'],
|
||||
]
|
||||
|
||||
# Platform -> RPC client mappings
|
||||
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
|
||||
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
|
||||
RPC_CLIENT_OPENGEAR = 'opengear'
|
||||
RPC_CLIENT_CHOICES = [
|
||||
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
|
||||
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
|
||||
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
|
||||
# Cable endpoint types
|
||||
CABLE_TERMINATION_TYPES = [
|
||||
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
|
||||
]
|
||||
|
||||
# Cable types
|
||||
CABLE_TYPE_CAT3 = 1300
|
||||
CABLE_TYPE_CAT5 = 1500
|
||||
CABLE_TYPE_CAT5E = 1510
|
||||
CABLE_TYPE_CAT6 = 1600
|
||||
CABLE_TYPE_CAT6A = 1610
|
||||
CABLE_TYPE_CAT7 = 1700
|
||||
CABLE_TYPE_MMF_OM1 = 3010
|
||||
CABLE_TYPE_MMF_OM2 = 3020
|
||||
CABLE_TYPE_MMF_OM3 = 3030
|
||||
CABLE_TYPE_MMF_OM4 = 3040
|
||||
CABLE_TYPE_SMF = 3500
|
||||
CABLE_TYPE_POWER = 5000
|
||||
CABLE_TYPE_CHOICES = (
|
||||
(
|
||||
'Copper', (
|
||||
(CABLE_TYPE_CAT3, 'CAT3'),
|
||||
(CABLE_TYPE_CAT5, 'CAT5'),
|
||||
(CABLE_TYPE_CAT5E, 'CAT5e'),
|
||||
(CABLE_TYPE_CAT6, 'CAT6'),
|
||||
(CABLE_TYPE_CAT6A, 'CAT6a'),
|
||||
(CABLE_TYPE_CAT7, 'CAT7'),
|
||||
),
|
||||
),
|
||||
(
|
||||
'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_POWER, 'Power'),
|
||||
)
|
||||
|
||||
CABLE_TERMINATION_TYPE_CHOICES = {
|
||||
# (API endpoint, human-friendly name)
|
||||
'consoleport': ('console-ports', 'Console port'),
|
||||
'consoleserverport': ('console-server-ports', 'Console server port'),
|
||||
'powerport': ('power-ports', 'Power port'),
|
||||
'poweroutlet': ('power-outlets', 'Power outlet'),
|
||||
'interface': ('interfaces', 'Interface'),
|
||||
'frontport': ('front-ports', 'Front panel port'),
|
||||
'rearport': ('rear-ports', 'Rear panel port'),
|
||||
}
|
||||
|
||||
COMPATIBLE_TERMINATION_TYPES = {
|
||||
'consoleport': ['consoleserverport', 'frontport', 'rearport'],
|
||||
'consoleserverport': ['consoleport', 'frontport', 'rearport'],
|
||||
'powerport': ['poweroutlet'],
|
||||
'poweroutlet': ['powerport'],
|
||||
'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
|
||||
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
|
||||
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
|
||||
'circuittermination': ['interface', 'frontport', 'rearport'],
|
||||
}
|
||||
|
||||
LENGTH_UNIT_METER = 'm'
|
||||
LENGTH_UNIT_CENTIMETER = 'cm'
|
||||
LENGTH_UNIT_MILLIMETER = 'mm'
|
||||
LENGTH_UNIT_FOOT = 'ft'
|
||||
LENGTH_UNIT_INCH = 'in'
|
||||
CABLE_LENGTH_UNIT_CHOICES = (
|
||||
(LENGTH_UNIT_METER, 'Meters'),
|
||||
(LENGTH_UNIT_CENTIMETER, 'Centimeters'),
|
||||
(LENGTH_UNIT_FOOT, 'Feet'),
|
||||
(LENGTH_UNIT_INCH, 'Inches'),
|
||||
)
|
||||
RACK_DIMENSION_UNIT_CHOICES = (
|
||||
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
|
||||
(LENGTH_UNIT_INCH, 'Inches'),
|
||||
)
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from netaddr import EUI, mac_unix_expanded
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
|
||||
from .formfields import MACAddressFormField
|
||||
from netaddr import AddrFormatError, EUI, mac_unix_expanded
|
||||
|
||||
|
||||
class ASNField(models.BigIntegerField):
|
||||
@@ -35,7 +30,7 @@ class MACAddressField(models.Field):
|
||||
return value
|
||||
try:
|
||||
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
||||
except ValueError as e:
|
||||
except AddrFormatError as e:
|
||||
raise ValidationError(e)
|
||||
|
||||
def db_type(self, connection):
|
||||
@@ -45,11 +40,3 @@ class MACAddressField(models.Field):
|
||||
if not value:
|
||||
return None
|
||||
return str(self.to_python(value))
|
||||
|
||||
def form_class(self):
|
||||
return MACAddressFormField
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': self.form_class()}
|
||||
defaults.update(kwargs)
|
||||
return super(MACAddressField, self).formfield(**defaults)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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
|
||||
@@ -10,15 +9,12 @@ from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableCharFieldFilter, NumericInFilter
|
||||
from virtualization.models import Cluster
|
||||
from .constants import (
|
||||
DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES,
|
||||
WIRELESS_IFACE_TYPES,
|
||||
)
|
||||
from .constants import *
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, 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,
|
||||
)
|
||||
|
||||
|
||||
@@ -32,7 +28,7 @@ class RegionFilter(django_filters.FilterSet):
|
||||
label='Parent region (ID)',
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
name='parent__slug',
|
||||
field_name='parent__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Parent region (slug)',
|
||||
@@ -53,7 +49,10 @@ class RegionFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -67,7 +66,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
name='region__slug',
|
||||
field_name='region__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
@@ -77,13 +76,13 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
name='tags__slug',
|
||||
field_name='tags__slug',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -112,12 +111,16 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class RackGroupFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@@ -127,6 +130,15 @@ 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):
|
||||
|
||||
@@ -136,7 +148,10 @@ class RackRoleFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -147,7 +162,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@@ -157,7 +172,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group__slug',
|
||||
field_name='group__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
@@ -167,28 +182,36 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=RACK_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role__slug',
|
||||
field_name='role__slug',
|
||||
queryset=RackRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
asset_tag = NullableCharFieldFilter()
|
||||
tag = django_filters.CharFilter(
|
||||
name='tags__slug',
|
||||
field_name='tags__slug',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['name', 'serial', 'type', 'width', 'u_height', 'desc_units']
|
||||
fields = [
|
||||
'name', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
||||
'outer_unit',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -197,12 +220,16 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
Q(name__icontains=value) |
|
||||
Q(facility_id__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class RackReservationFilter(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -212,23 +239,23 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
label='Rack (ID)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
field_name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site__slug',
|
||||
field_name='rack__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
field_name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__group__slug',
|
||||
field_name='rack__group__slug',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
@@ -238,7 +265,7 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
@@ -248,7 +275,7 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
label='User (ID)',
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
name='user',
|
||||
field_name='user',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User (name)',
|
||||
@@ -277,7 +304,10 @@ class ManufacturerFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -287,20 +317,43 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='manufacturer__slug',
|
||||
field_name='manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
console_ports = django_filters.CharFilter(
|
||||
method='_console_ports',
|
||||
label='Has console ports',
|
||||
)
|
||||
console_server_ports = django_filters.CharFilter(
|
||||
method='_console_server_ports',
|
||||
label='Has console server ports',
|
||||
)
|
||||
power_ports = django_filters.CharFilter(
|
||||
method='_power_ports',
|
||||
label='Has power ports',
|
||||
)
|
||||
power_outlets = django_filters.CharFilter(
|
||||
method='_power_outlets',
|
||||
label='Has power outlets',
|
||||
)
|
||||
interfaces = django_filters.CharFilter(
|
||||
method='_interfaces',
|
||||
label='Has interfaces',
|
||||
)
|
||||
pass_through_ports = django_filters.CharFilter(
|
||||
method='_pass_through_ports',
|
||||
label='Has pass-through ports',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
name='tags__slug',
|
||||
field_name='tags__slug',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role',
|
||||
'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -313,11 +366,38 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
def _console_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(consoleport_templates__isnull=bool(value))
|
||||
|
||||
def _console_server_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(consoleserverport_templates__isnull=bool(value))
|
||||
|
||||
def _power_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(powerport_templates__isnull=bool(value))
|
||||
|
||||
def _power_outlets(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(poweroutlet_templates__isnull=bool(value))
|
||||
|
||||
def _interfaces(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(interface_templates__isnull=bool(value))
|
||||
|
||||
def _pass_through_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(
|
||||
frontport_templates__isnull=bool(value),
|
||||
rearport_templates__isnull=bool(value)
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
name='device_type_id',
|
||||
field_name='device_type_id',
|
||||
label='Device type (ID)',
|
||||
)
|
||||
|
||||
@@ -357,6 +437,20 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
fields = ['name', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
class FrontPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['name', 'type']
|
||||
|
||||
|
||||
class RearPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['name', 'type']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -373,12 +467,12 @@ class DeviceRoleFilter(django_filters.FilterSet):
|
||||
|
||||
class PlatformFilter(django_filters.FilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='manufacturer',
|
||||
field_name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='manufacturer__slug',
|
||||
field_name='manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
@@ -390,18 +484,21 @@ class PlatformFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer',
|
||||
field_name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer__slug',
|
||||
field_name='device_type__manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
@@ -411,12 +508,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Device type (ID)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role_id',
|
||||
field_name='device_role_id',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role__slug',
|
||||
field_name='device_role__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
@@ -426,7 +523,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant__slug',
|
||||
field_name='tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
@@ -436,30 +533,40 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Platform (ID)',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platform__slug',
|
||||
field_name='platform__slug',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
name = NullableCharFieldFilter()
|
||||
asset_tag = NullableCharFieldFilter()
|
||||
region_id = django_filters.NumberFilter(
|
||||
method='filter_region',
|
||||
field_name='pk',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = django_filters.CharFilter(
|
||||
method='filter_region',
|
||||
field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
field_name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack',
|
||||
field_name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
@@ -468,7 +575,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='VM cluster (ID)',
|
||||
)
|
||||
model = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__slug',
|
||||
field_name='device_type__slug',
|
||||
queryset=DeviceType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Device model (slug)',
|
||||
@@ -478,20 +585,32 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
null_value=None
|
||||
)
|
||||
is_full_depth = django_filters.BooleanFilter(
|
||||
name='device_type__is_full_depth',
|
||||
field_name='device_type__is_full_depth',
|
||||
label='Is full depth',
|
||||
)
|
||||
is_console_server = django_filters.BooleanFilter(
|
||||
name='device_type__is_console_server',
|
||||
label='Is a console server',
|
||||
console_ports = django_filters.CharFilter(
|
||||
method='_console_ports',
|
||||
label='Has console ports',
|
||||
)
|
||||
is_pdu = django_filters.BooleanFilter(
|
||||
name='device_type__is_pdu',
|
||||
label='Is a PDU',
|
||||
console_server_ports = django_filters.CharFilter(
|
||||
method='_console_server_ports',
|
||||
label='Has console server ports',
|
||||
)
|
||||
is_network_device = django_filters.BooleanFilter(
|
||||
name='device_type__is_network_device',
|
||||
label='Is a network device',
|
||||
power_ports = django_filters.CharFilter(
|
||||
method='_power_ports',
|
||||
label='Has power ports',
|
||||
)
|
||||
power_outlets = django_filters.CharFilter(
|
||||
method='_power_outlets',
|
||||
label='Has power outlets',
|
||||
)
|
||||
interfaces = django_filters.CharFilter(
|
||||
method='_interfaces',
|
||||
label='Has interfaces',
|
||||
)
|
||||
pass_through_ports = django_filters.CharFilter(
|
||||
method='_pass_through_ports',
|
||||
label='Has pass-through ports',
|
||||
)
|
||||
mac_address = django_filters.CharFilter(
|
||||
method='_mac_address',
|
||||
@@ -502,12 +621,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Has a primary IP',
|
||||
)
|
||||
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='virtual_chassis',
|
||||
field_name='virtual_chassis',
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
label='Virtual chassis (ID)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
name='tags__slug',
|
||||
field_name='tags__slug',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -525,6 +644,16 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
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:
|
||||
@@ -547,6 +676,33 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
Q(primary_ip6__isnull=False)
|
||||
)
|
||||
|
||||
def _console_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(consoleports__isnull=bool(value))
|
||||
|
||||
def _console_server_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(consoleserverports__isnull=bool(value))
|
||||
|
||||
def _power_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(powerports__isnull=bool(value))
|
||||
|
||||
def _power_outlets(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(poweroutlets__isnull=bool(value))
|
||||
|
||||
def _interfaces(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(interfaces__isnull=bool(value))
|
||||
|
||||
def _pass_through_ports(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
return queryset.exclude(
|
||||
frontports__isnull=bool(value),
|
||||
rearports__isnull=bool(value)
|
||||
)
|
||||
|
||||
|
||||
class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelChoiceFilter(
|
||||
@@ -559,7 +715,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
label='Device (name)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
name='tags__slug',
|
||||
field_name='tags__slug',
|
||||
)
|
||||
|
||||
|
||||
@@ -598,12 +754,12 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
"""
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
name='name',
|
||||
field_name='name',
|
||||
label='Device',
|
||||
)
|
||||
device_id = django_filters.NumberFilter(
|
||||
method='filter_device',
|
||||
name='pk',
|
||||
field_name='pk',
|
||||
label='Device (ID)',
|
||||
)
|
||||
type = django_filters.CharFilter(
|
||||
@@ -611,7 +767,7 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
label='Interface type',
|
||||
)
|
||||
lag_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='lag',
|
||||
field_name='lag',
|
||||
queryset=Interface.objects.all(),
|
||||
label='LAG interface (ID)',
|
||||
)
|
||||
@@ -620,7 +776,15 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
label='MAC address',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
name='tags__slug',
|
||||
field_name='tags__slug',
|
||||
)
|
||||
vlan_id = django_filters.CharFilter(
|
||||
method='filter_vlan_id',
|
||||
label='Assigned VLAN'
|
||||
)
|
||||
vlan = django_filters.CharFilter(
|
||||
method='filter_vlan',
|
||||
label='Assigned VID'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -629,13 +793,30 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
try:
|
||||
device = Device.objects.select_related('device_type').get(**{name: value})
|
||||
device = Device.objects.get(**{name: value})
|
||||
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
|
||||
ordering = device.device_type.interface_ordering
|
||||
return queryset.filter(pk__in=vc_interface_ids).order_naturally(ordering)
|
||||
return queryset.filter(pk__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
def filter_vlan_id(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(untagged_vlan_id=value) |
|
||||
Q(tagged_vlans=value)
|
||||
)
|
||||
|
||||
def filter_vlan(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(untagged_vlan_id__vid=value) |
|
||||
Q(tagged_vlans__vid=value)
|
||||
)
|
||||
|
||||
def filter_type(self, queryset, name, value):
|
||||
value = value.strip().lower()
|
||||
return {
|
||||
@@ -656,6 +837,20 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class FrontPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['name', 'type']
|
||||
|
||||
|
||||
class RearPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['name', 'type']
|
||||
|
||||
|
||||
class DeviceBayFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
@@ -668,6 +863,15 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
device_id = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
label='Parent inventory item (ID)',
|
||||
@@ -677,7 +881,7 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='manufacturer__slug',
|
||||
field_name='manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
@@ -707,29 +911,29 @@ class VirtualChassisFilter(django_filters.FilterSet):
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='master__site',
|
||||
field_name='master__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='master__site__slug',
|
||||
field_name='master__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='master__tenant',
|
||||
field_name='master__tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='master__tenant__slug',
|
||||
field_name='master__tenant__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = django_filters.CharFilter(
|
||||
name='tags__slug',
|
||||
field_name='tags__slug',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -746,6 +950,22 @@ class VirtualChassisFilter(django_filters.FilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class CableFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['type', 'status', 'color', 'length', 'length_unit']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(label__icontains=value)
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
@@ -763,14 +983,14 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(cs_port__device__site__slug=value)
|
||||
return queryset.filter(connected_endpoint__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(cs_port__device__name__icontains=value)
|
||||
Q(connected_endpoint__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
@@ -791,14 +1011,14 @@ class PowerConnectionFilter(django_filters.FilterSet):
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(power_outlet__device__site__slug=value)
|
||||
return queryset.filter(connected_endpoint__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(power_outlet__device__name__icontains=value)
|
||||
Q(connected_endpoint__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
@@ -813,21 +1033,21 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
model = Interface
|
||||
fields = ['connection_status']
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__site__slug=value) |
|
||||
Q(interface_b__device__site__slug=value)
|
||||
Q(device__site__slug=value) |
|
||||
Q(_connected_interface__device__site__slug=value)
|
||||
)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__name__icontains=value) |
|
||||
Q(interface_b__device__name__icontains=value)
|
||||
Q(device__name__icontains=value) |
|
||||
Q(_connected_interface__device__name__icontains=value)
|
||||
)
|
||||
|
||||
@@ -76,10 +76,7 @@
|
||||
"model": "MX960",
|
||||
"slug": "mx960",
|
||||
"u_height": 16,
|
||||
"is_full_depth": true,
|
||||
"is_console_server": false,
|
||||
"is_pdu": false,
|
||||
"is_network_device": true
|
||||
"is_full_depth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -92,10 +89,7 @@
|
||||
"model": "EX9214",
|
||||
"slug": "ex9214",
|
||||
"u_height": 16,
|
||||
"is_full_depth": true,
|
||||
"is_console_server": false,
|
||||
"is_pdu": false,
|
||||
"is_network_device": true
|
||||
"is_full_depth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -108,10 +102,7 @@
|
||||
"model": "QFX5100-24Q",
|
||||
"slug": "qfx5100-24q",
|
||||
"u_height": 1,
|
||||
"is_full_depth": true,
|
||||
"is_console_server": false,
|
||||
"is_pdu": false,
|
||||
"is_network_device": true
|
||||
"is_full_depth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -124,10 +115,7 @@
|
||||
"model": "QFX5100-48S",
|
||||
"slug": "qfx5100-48s",
|
||||
"u_height": 1,
|
||||
"is_full_depth": true,
|
||||
"is_console_server": false,
|
||||
"is_pdu": false,
|
||||
"is_network_device": true
|
||||
"is_full_depth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -140,10 +128,7 @@
|
||||
"model": "CM4148",
|
||||
"slug": "cm4148",
|
||||
"u_height": 1,
|
||||
"is_full_depth": true,
|
||||
"is_console_server": true,
|
||||
"is_pdu": false,
|
||||
"is_network_device": false
|
||||
"is_full_depth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -156,10 +141,7 @@
|
||||
"model": "CWG-24VYM415C9",
|
||||
"slug": "cwg-24vym415c9",
|
||||
"u_height": 0,
|
||||
"is_full_depth": false,
|
||||
"is_console_server": false,
|
||||
"is_pdu": true,
|
||||
"is_network_device": false
|
||||
"is_full_depth": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1903,8 +1885,7 @@
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Juniper Junos",
|
||||
"slug": "juniper-junos",
|
||||
"rpc_client": "juniper-junos"
|
||||
"slug": "juniper-junos"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1912,8 +1893,7 @@
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Opengear",
|
||||
"slug": "opengear",
|
||||
"rpc_client": "opengear"
|
||||
"slug": "opengear"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2153,7 +2133,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "Console (RE0)",
|
||||
"cs_port": 27,
|
||||
"connected_endpoint": 27,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2163,7 +2143,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "Console (RE1)",
|
||||
"cs_port": 38,
|
||||
"connected_endpoint": 38,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2173,7 +2153,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "Console (RE0)",
|
||||
"cs_port": 5,
|
||||
"connected_endpoint": 5,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2183,7 +2163,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "Console (RE1)",
|
||||
"cs_port": 16,
|
||||
"connected_endpoint": 16,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2193,7 +2173,7 @@
|
||||
"fields": {
|
||||
"device": 3,
|
||||
"name": "Console",
|
||||
"cs_port": 49,
|
||||
"connected_endpoint": 49,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2203,7 +2183,7 @@
|
||||
"fields": {
|
||||
"device": 4,
|
||||
"name": "Console",
|
||||
"cs_port": 48,
|
||||
"connected_endpoint": 48,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2213,7 +2193,7 @@
|
||||
"fields": {
|
||||
"device": 5,
|
||||
"name": "Console",
|
||||
"cs_port": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2223,7 +2203,7 @@
|
||||
"fields": {
|
||||
"device": 6,
|
||||
"name": "Console",
|
||||
"cs_port": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2233,7 +2213,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "Console (RE0)",
|
||||
"cs_port": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2243,7 +2223,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "Console (RE1)",
|
||||
"cs_port": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2253,7 +2233,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "Console (RE0)",
|
||||
"cs_port": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2263,7 +2243,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "Console (RE1)",
|
||||
"cs_port": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2273,7 +2253,7 @@
|
||||
"fields": {
|
||||
"device": 9,
|
||||
"name": "Console",
|
||||
"cs_port": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2283,7 +2263,7 @@
|
||||
"fields": {
|
||||
"device": 11,
|
||||
"name": "Serial",
|
||||
"cs_port": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2293,7 +2273,7 @@
|
||||
"fields": {
|
||||
"device": 12,
|
||||
"name": "Serial",
|
||||
"cs_port": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2687,7 +2667,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "PEM0",
|
||||
"power_outlet": 25,
|
||||
"connected_endpoint": 25,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2697,7 +2677,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "PEM1",
|
||||
"power_outlet": 49,
|
||||
"connected_endpoint": 49,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2707,7 +2687,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "PEM2",
|
||||
"power_outlet": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2717,7 +2697,7 @@
|
||||
"fields": {
|
||||
"device": 1,
|
||||
"name": "PEM3",
|
||||
"power_outlet": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2727,7 +2707,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "PEM0",
|
||||
"power_outlet": 26,
|
||||
"connected_endpoint": 26,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2737,7 +2717,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "PEM1",
|
||||
"power_outlet": 50,
|
||||
"connected_endpoint": 50,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2747,7 +2727,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "PEM2",
|
||||
"power_outlet": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2757,7 +2737,7 @@
|
||||
"fields": {
|
||||
"device": 2,
|
||||
"name": "PEM3",
|
||||
"power_outlet": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2767,7 +2747,7 @@
|
||||
"fields": {
|
||||
"device": 4,
|
||||
"name": "PSU0",
|
||||
"power_outlet": 28,
|
||||
"connected_endpoint": 28,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2777,7 +2757,7 @@
|
||||
"fields": {
|
||||
"device": 4,
|
||||
"name": "PSU1",
|
||||
"power_outlet": 52,
|
||||
"connected_endpoint": 52,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2787,7 +2767,7 @@
|
||||
"fields": {
|
||||
"device": 5,
|
||||
"name": "PSU0",
|
||||
"power_outlet": 56,
|
||||
"connected_endpoint": 56,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2797,7 +2777,7 @@
|
||||
"fields": {
|
||||
"device": 5,
|
||||
"name": "PSU1",
|
||||
"power_outlet": 32,
|
||||
"connected_endpoint": 32,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2807,7 +2787,7 @@
|
||||
"fields": {
|
||||
"device": 3,
|
||||
"name": "PSU0",
|
||||
"power_outlet": 27,
|
||||
"connected_endpoint": 27,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2817,7 +2797,7 @@
|
||||
"fields": {
|
||||
"device": 3,
|
||||
"name": "PSU1",
|
||||
"power_outlet": 51,
|
||||
"connected_endpoint": 51,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2827,7 +2807,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "PEM0",
|
||||
"power_outlet": 53,
|
||||
"connected_endpoint": 53,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2837,7 +2817,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "PEM1",
|
||||
"power_outlet": 29,
|
||||
"connected_endpoint": 29,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2847,7 +2827,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "PEM2",
|
||||
"power_outlet": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2857,7 +2837,7 @@
|
||||
"fields": {
|
||||
"device": 7,
|
||||
"name": "PEM3",
|
||||
"power_outlet": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2867,7 +2847,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "PEM0",
|
||||
"power_outlet": 54,
|
||||
"connected_endpoint": 54,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2877,7 +2857,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "PEM1",
|
||||
"power_outlet": 30,
|
||||
"connected_endpoint": 30,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2887,7 +2867,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "PEM2",
|
||||
"power_outlet": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2897,7 +2877,7 @@
|
||||
"fields": {
|
||||
"device": 8,
|
||||
"name": "PEM3",
|
||||
"power_outlet": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2907,7 +2887,7 @@
|
||||
"fields": {
|
||||
"device": 6,
|
||||
"name": "PSU0",
|
||||
"power_outlet": 55,
|
||||
"connected_endpoint": 55,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2917,7 +2897,7 @@
|
||||
"fields": {
|
||||
"device": 6,
|
||||
"name": "PSU1",
|
||||
"power_outlet": 31,
|
||||
"connected_endpoint": 31,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -2927,7 +2907,7 @@
|
||||
"fields": {
|
||||
"device": 9,
|
||||
"name": "PSU",
|
||||
"power_outlet": null,
|
||||
"connected_endpoint": null,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
@@ -5748,158 +5728,5 @@
|
||||
"mgmt_only": true,
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"interface_a": 99,
|
||||
"interface_b": 15,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"interface_a": 100,
|
||||
"interface_b": 153,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"interface_a": 46,
|
||||
"interface_b": 14,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"interface_a": 47,
|
||||
"interface_b": 152,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"interface_a": 91,
|
||||
"interface_b": 144,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"interface_a": 92,
|
||||
"interface_b": 145,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 16,
|
||||
"fields": {
|
||||
"interface_a": 189,
|
||||
"interface_b": 37,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 17,
|
||||
"fields": {
|
||||
"interface_a": 192,
|
||||
"interface_b": 175,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 18,
|
||||
"fields": {
|
||||
"interface_a": 195,
|
||||
"interface_b": 41,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 19,
|
||||
"fields": {
|
||||
"interface_a": 198,
|
||||
"interface_b": 179,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 20,
|
||||
"fields": {
|
||||
"interface_a": 191,
|
||||
"interface_b": 197,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 21,
|
||||
"fields": {
|
||||
"interface_a": 194,
|
||||
"interface_b": 200,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 22,
|
||||
"fields": {
|
||||
"interface_a": 9,
|
||||
"interface_b": 218,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 23,
|
||||
"fields": {
|
||||
"interface_a": 8,
|
||||
"interface_b": 206,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 24,
|
||||
"fields": {
|
||||
"interface_a": 7,
|
||||
"interface_b": 212,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 25,
|
||||
"fields": {
|
||||
"interface_a": 217,
|
||||
"interface_b": 205,
|
||||
"connection_status": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.interfaceconnection",
|
||||
"pk": 26,
|
||||
"fields": {
|
||||
"interface_a": 216,
|
||||
"interface_b": 211,
|
||||
"connection_status": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -149,8 +149,7 @@
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Cisco IOS",
|
||||
"slug": "cisco-ios",
|
||||
"rpc_client": "cisco-ios"
|
||||
"slug": "cisco-ios"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -158,8 +157,7 @@
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Cisco NX-OS",
|
||||
"slug": "cisco-nx-os",
|
||||
"rpc_client": ""
|
||||
"slug": "cisco-nx-os"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -167,8 +165,7 @@
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Juniper Junos",
|
||||
"slug": "juniper-junos",
|
||||
"rpc_client": "juniper-junos"
|
||||
"slug": "juniper-junos"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -176,8 +173,7 @@
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Arista EOS",
|
||||
"slug": "arista-eos",
|
||||
"rpc_client": ""
|
||||
"slug": "arista-eos"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -185,8 +181,7 @@
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Linux",
|
||||
"slug": "linux",
|
||||
"rpc_client": ""
|
||||
"slug": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -194,8 +189,7 @@
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Opengear",
|
||||
"slug": "opengear",
|
||||
"rpc_client": "opengear"
|
||||
"slug": "opengear"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from netaddr import EUI, AddrFormatError
|
||||
|
||||
|
||||
#
|
||||
# Form fields
|
||||
#
|
||||
|
||||
class MACAddressFormField(forms.Field):
|
||||
default_error_messages = {
|
||||
'invalid': "Enter a valid MAC address.",
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
if isinstance(value, EUI):
|
||||
return value
|
||||
|
||||
try:
|
||||
return EUI(value, version=48)
|
||||
except AddrFormatError:
|
||||
raise ValidationError("Please specify a valid MAC address.")
|
||||
1178
netbox/dcim/forms.py
1178
netbox/dcim/forms.py
File diff suppressed because it is too large
Load Diff
85
netbox/dcim/managers.py
Normal file
85
netbox/dcim/managers.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from django.db.models import Manager, QuerySet
|
||||
from django.db.models.expressions import RawSQL
|
||||
|
||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||
|
||||
# Regular expressions for parsing Interface names
|
||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9\.:]+)')"
|
||||
SLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})/') AS integer), NULL)"
|
||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?\d{{1,9}}/(\d{{1,9}})') AS integer), NULL)"
|
||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{2}}(\d{{1,9}})') AS integer), NULL)"
|
||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{3}}(\d{{1,9}})') AS integer), NULL)"
|
||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?(\d{{1,9}})([^/]|$)') AS integer)"
|
||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
|
||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
|
||||
|
||||
|
||||
class DeviceComponentManager(Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
queryset = super(DeviceComponentManager, self).get_queryset()
|
||||
table_name = self.model._meta.db_table
|
||||
sql = r"CONCAT(REGEXP_REPLACE({}.name, '\d+$', ''), LPAD(SUBSTRING({}.name FROM '\d+$'), 8, '0'))"
|
||||
|
||||
# Pad any trailing digits to effect natural sorting
|
||||
return queryset.extra(
|
||||
select={
|
||||
'name_padded': sql.format(table_name, table_name),
|
||||
}
|
||||
).order_by('name_padded')
|
||||
|
||||
|
||||
class InterfaceQuerySet(QuerySet):
|
||||
|
||||
def connectable(self):
|
||||
"""
|
||||
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
|
||||
wireless).
|
||||
"""
|
||||
return self.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES)
|
||||
|
||||
|
||||
class InterfaceManager(Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Naturally order interfaces by their type and numeric position. To order interfaces naturally, the `name` field
|
||||
is split into eight distinct components: leading text (type), slot, subslot, position, subposition, ID, channel,
|
||||
and virtual circuit:
|
||||
|
||||
{type}{slot or ID}/{subslot}/{position}/{subposition}:{channel}.{vc}
|
||||
|
||||
Components absent from the interface name are coalesced to zero or null. For example, an interface named
|
||||
GigabitEthernet1/2/3 would be parsed as follows:
|
||||
|
||||
type = 'GigabitEthernet'
|
||||
slot = 1
|
||||
subslot = 2
|
||||
position = 3
|
||||
subposition = None
|
||||
id = None
|
||||
channel = 0
|
||||
vc = 0
|
||||
|
||||
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
|
||||
match any of the prescribed fields.
|
||||
"""
|
||||
|
||||
sql_col = '{}.name'.format(self.model._meta.db_table)
|
||||
ordering = [
|
||||
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name',
|
||||
]
|
||||
|
||||
fields = {
|
||||
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
||||
'_id': RawSQL(ID_RE.format(sql_col), []),
|
||||
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
|
||||
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
|
||||
'_position': RawSQL(POSITION_RE.format(sql_col), []),
|
||||
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
|
||||
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
|
||||
'_vc': RawSQL(VC_RE.format(sql_col), []),
|
||||
}
|
||||
|
||||
return InterfaceQuerySet(self.model, using=self._db).annotate(**fields).order_by(*ordering)
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-06-22 18:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-06-22 18:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-06-28 17:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-01 20:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-06 17:22
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-11 18:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-11 18:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-11 19:01
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-13 19:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-14 21:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-26 15:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-26 21:59
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-06 20:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-08 21:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-09 21:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-10 13:45
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-10 14:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-08-11 15:42
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-09-13 15:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-10-28 15:01
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-10-31 18:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-12-06 16:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-12-16 16:08
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2016-12-29 16:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-01-06 16:56
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 18:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 21:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 21:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 21:25
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-27 19:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-28 17:14
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-03-02 15:09
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.6 on 2017-03-17 18:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.6 on 2017-03-21 14:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-08 15:57
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-09 16:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import dcim.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-06-16 21:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-06-23 17:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user