Merge upstream v2.8.5 into develop

# Conflicts:
#	netbox/dcim/api/serializers.py
#	netbox/dcim/models/__init__.py
#	netbox/extras/constants.py
#	netbox/netbox/admin.py
#	netbox/templates/users/base.html
#	requirements.txt
This commit is contained in:
Marco Ceppi 2020-05-27 15:44:47 -04:00
commit bb7191e953
343 changed files with 9806 additions and 8232 deletions

View File

@ -15,24 +15,24 @@ about: Report a reproducible bug in the current release of NetBox
Please describe the environment in which you are running NetBox. Be sure Please describe the environment in which you are running NetBox. Be sure
that you are running an unmodified instance of the latest stable release that you are running an unmodified instance of the latest stable release
before submitting a bug report. before submitting a bug report, and that any plugins have been disabled.
--> -->
### Environment ### Environment
* Python version: <!-- Example: 3.6.9 --> * Python version:
* NetBox version: <!-- Example: 2.7.3 --> * NetBox version:
<!-- <!--
Describe in detail the exact steps that someone else can take to reproduce Describe in detail the exact steps that someone else can take to reproduce
this bug using the current stable release of NetBox (or the current beta this bug using the current stable release of NetBox. Begin with the
release where applicable). Begin with the creation of any necessary creation of any necessary database objects and call out every operation
database objects and call out every operation being performed explicitly. being performed explicitly. If reporting a bug in the REST API, be sure to
If reporting a bug in the REST API, be sure to reconstruct the raw HTTP reconstruct the raw HTTP request(s) being made: Don't rely on a client
request(s) being made: Don't rely on a wrapper like pynetbox. library such as pynetbox.
--> -->
### Steps to Reproduce ### Steps to Reproduce
1. 1.
2. 2.
3. 3.
<!-- What did you expect to happen? --> <!-- What did you expect to happen? -->
### Expected Behavior ### Expected Behavior

View File

@ -3,10 +3,9 @@ services:
- postgresql - postgresql
- redis-server - redis-server
addons: addons:
postgresql: "9.4" postgresql: "9.6"
language: python language: python
python: python:
- "3.5"
- "3.6" - "3.6"
- "3.7" - "3.7"
install: install:

View File

@ -1,7 +1,5 @@
![NetBox](docs/netbox_logo.svg "NetBox logo") ![NetBox](docs/netbox_logo.svg "NetBox logo")
**The [2020 NetBox user survey](https://docs.google.com/forms/d/1OVZuC4kQ-6kJbVf0bDB6vgkL9H96xF6phvYzby23elk/edit) is open!** Your feedback helps guide the project's long-term development.
NetBox is an IP address management (IPAM) and data center infrastructure NetBox is an IP address management (IPAM) and data center infrastructure
management (DCIM) tool. Initially conceived by the network engineering team at management (DCIM) tool. Initially conceived by the network engineering team at
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically

View File

@ -68,8 +68,7 @@ Jinja2
# Simple markup language for rendering HTML # Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown # https://github.com/Python-Markdown/markdown
# py-gfm requires Markdown<3.0 Markdown
Markdown<3.0
# Library for manipulating IP prefixes and addresses # Library for manipulating IP prefixes and addresses
# https://github.com/drkjam/netaddr # https://github.com/drkjam/netaddr

26
contrib/apache.conf Normal file
View File

@ -0,0 +1,26 @@
<VirtualHost *:443>
ProxyPreserveHost On
# CHANGE THIS TO YOUR SERVER'S NAME
ServerName netbox.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/netbox.crt
SSLCertificateKeyFile /etc/ssl/private/netbox.key
Alias /static /opt/netbox/netbox/static
<Directory /opt/netbox/netbox/static>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Require all granted
</Directory>
<Location /static>
ProxyPass !
</Location>
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
ProxyPass / http://127.0.0.1:8001/
ProxyPassReverse / http://127.0.0.1:8001/
</VirtualHost>

29
contrib/nginx.conf Normal file
View File

@ -0,0 +1,29 @@
server {
listen 443 ssl;
# CHANGE THIS TO YOUR SERVER'S NAME
server_name netbox.example.com;
ssl_certificate /etc/ssl/certs/netbox.crt;
ssl_certificate_key /etc/ssl/private/netbox.key;
client_max_body_size 25m;
location /static/ {
alias /opt/netbox/netbox/static/;
}
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
# Redirect HTTP traffic to HTTPS
listen 80;
server_name _;
return 301 https://$host$request_uri;
}

View File

@ -51,7 +51,7 @@ SECRET_KEY = os.environ.get('SECRET_KEY', read_secret('secret_key'))
# Redis database settings. The Redis database is used for caching and background processing such as webhooks # Redis database settings. The Redis database is used for caching and background processing such as webhooks
REDIS = { REDIS = {
'webhooks': { 'tasks': {
'HOST': os.environ.get('REDIS_HOST', 'localhost'), 'HOST': os.environ.get('REDIS_HOST', 'localhost'),
'PORT': int(os.environ.get('REDIS_PORT', 6379)), 'PORT': int(os.environ.get('REDIS_PORT', 6379)),
'PASSWORD': os.environ.get('REDIS_PASSWORD', read_secret('redis_password')), 'PASSWORD': os.environ.get('REDIS_PASSWORD', read_secret('redis_password')),
@ -119,6 +119,10 @@ EMAIL = {
'PASSWORD': os.environ.get('EMAIL_PASSWORD', read_secret('email_password')), 'PASSWORD': os.environ.get('EMAIL_PASSWORD', read_secret('email_password')),
'TIMEOUT': int(os.environ.get('EMAIL_TIMEOUT', 10)), # seconds 'TIMEOUT': int(os.environ.get('EMAIL_TIMEOUT', 10)), # seconds
'FROM_EMAIL': os.environ.get('EMAIL_FROM', ''), 'FROM_EMAIL': os.environ.get('EMAIL_FROM', ''),
'USE_SSL': os.environ.get('EMAIL_USE_SSL', 'False').lower() == 'true',
'USE_TLS': os.environ.get('EMAIL_USE_TLS', 'False').lower() == 'true',
'SSL_CERTFILE': os.environ.get('EMAIL_SSL_CERTFILE', ''),
'SSL_KEYFILE': os.environ.get('EMAIL_SSL_KEYFILE', ''),
} }
# Enforcement of unique IP space can be toggled on a per-VRF basis. # Enforcement of unique IP space can be toggled on a per-VRF basis.
@ -170,6 +174,15 @@ PAGINATE_COUNT = int(os.environ.get('PAGINATE_COUNT', 50))
# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to # When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
# prefer IPv4 instead. # prefer IPv4 instead.
PREFER_IPV4 = os.environ.get('PREFER_IPV4', 'False').lower() == 'true' PREFER_IPV4 = os.environ.get('PREFER_IPV4', 'False').lower() == 'true'
# This determines how often the GitHub API is called to check the latest release of NetBox in seconds. Must be at least 1 hour.
RELEASE_CHECK_TIMEOUT = os.environ.get('RELEASE_CHECK_TIMEOUT', 24 * 3600)
# This repository is used to check whether there is a new release of NetBox available. Set to None to disable the
# version check or use the URL below to check for release in the official NetBox repository.
# https://api.github.com/repos/netbox-community/netbox/releases
RELEASE_CHECK_URL = os.environ.get('RELEASE_CHECK_URL', None)
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
# this setting is derived from the installed location. # this setting is derived from the installed location.
REPORTS_ROOT = os.environ.get('REPORTS_ROOT', '/etc/netbox/reports') REPORTS_ROOT = os.environ.get('REPORTS_ROOT', '/etc/netbox/reports')

View File

@ -1,6 +1,6 @@
from dcim.models import RackRole from dcim.models import RackRole
from ruamel.yaml import YAML from ruamel.yaml import YAML
from utilities.forms import COLOR_CHOICES from utilities.choices import ColorChoices
from pathlib import Path from pathlib import Path
import sys import sys
@ -18,7 +18,7 @@ with file.open('r') as stream:
if 'color' in params: if 'color' in params:
color = params.pop('color') color = params.pop('color')
for color_tpl in COLOR_CHOICES: for color_tpl in ColorChoices:
if color in color_tpl: if color in color_tpl:
params['color'] = color_tpl[0] params['color'] = color_tpl[0]

View File

@ -1,6 +1,6 @@
from dcim.models import DeviceRole from dcim.models import DeviceRole
from ruamel.yaml import YAML from ruamel.yaml import YAML
from utilities.forms import COLOR_CHOICES from utilities.choices import ColorChoices
from pathlib import Path from pathlib import Path
import sys import sys
@ -19,7 +19,7 @@ with file.open('r') as stream:
if 'color' in params: if 'color' in params:
color = params.pop('color') color = params.pop('color')
for color_tpl in COLOR_CHOICES: for color_tpl in ColorChoices:
if color in color_tpl: if color in color_tpl:
params['color'] = color_tpl[0] params['color'] = color_tpl[0]

View File

@ -63,7 +63,7 @@ A human-friendly description of what your script does.
### `field_order` ### `field_order`
A list of field names indicating the order in which the form fields should appear. This is optional, however on Python 3.5 and earlier the fields will appear in random order. (Declarative ordering is preserved on Python 3.6 and above.) For example: A list of field names indicating the order in which the form fields should appear. This is optional, and should not be required on Python 3.6 and above. For example:
``` ```
field_order = ['var1', 'var2', 'var3'] field_order = ['var1', 'var2', 'var3']

View File

@ -32,3 +32,7 @@ This can be setup by first creating a shared directory and then adding this line
``` ```
environment=prometheus_multiproc_dir=/tmp/prometheus_metrics environment=prometheus_multiproc_dir=/tmp/prometheus_metrics
``` ```
#### Accuracy
If having accurate long-term metrics in a multiprocess environment is important to you then it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using Netbox with gunicorn in a containerized enviroment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).

View File

@ -33,7 +33,6 @@ Within each report class, we'll create a number of test methods to execute our r
``` ```
from dcim.choices import DeviceStatusChoices from dcim.choices import DeviceStatusChoices
from dcim.constants import CONNECTION_STATUS_PLANNED
from dcim.models import ConsolePort, Device, PowerPort from dcim.models import ConsolePort, Device, PowerPort
from extras.reports import Report from extras.reports import Report
@ -51,7 +50,7 @@ class DeviceConnectionsReport(Report):
console_port.device, console_port.device,
"No console connection defined for {}".format(console_port.name) "No console connection defined for {}".format(console_port.name)
) )
elif console_port.connection_status == CONNECTION_STATUS_PLANNED: elif not console_port.connection_status:
self.log_warning( self.log_warning(
console_port.device, console_port.device,
"Console connection for {} marked as planned".format(console_port.name) "Console connection for {} marked as planned".format(console_port.name)
@ -67,7 +66,7 @@ class DeviceConnectionsReport(Report):
for power_port in PowerPort.objects.filter(device=device): for power_port in PowerPort.objects.filter(device=device):
if power_port.connected_endpoint is not None: if power_port.connected_endpoint is not None:
connected_ports += 1 connected_ports += 1
if power_port.connection_status == CONNECTION_STATUS_PLANNED: if not power_port.connection_status:
self.log_warning( self.log_warning(
device, device,
"Power connection for {} marked as planned".format(power_port.name) "Power connection for {} marked as planned".format(power_port.name)

View File

@ -71,3 +71,36 @@ If no body template is specified, the request body will be populated with a JSON
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under Django RQ > Queues. When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under Django RQ > Queues.
A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI. A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI.
## Troubleshooting
To assist with verifying that the content of outgoing webhooks is rendered correctly, NetBox provides a simple HTTP listener that can be run locally to receive and display webhook requests. First, modify the target URL of the desired webhook to `http://localhost:9000/`. This will instruct NetBox to send the request to the local server on TCP port 9000. Then, start the webhook receiver service from the NetBox root directory:
```no-highlight
$ python netbox/manage.py webhook_receiver
Listening on port http://localhost:9000. Stop with CONTROL-C.
```
You can test the receiver itself by sending any HTTP request to it. For example:
```no-highlight
$ curl -X POST http://localhost:9000 --data '{"foo": "bar"}'
```
The server will print output similar to the following:
```no-highlight
[1] Tue, 07 Apr 2020 17:44:02 GMT 127.0.0.1 "POST / HTTP/1.1" 200 -
Host: localhost:9000
User-Agent: curl/7.58.0
Accept: */*
Content-Length: 14
Content-Type: application/x-www-form-urlencoded
{"foo": "bar"}
------------
```
Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection.
Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI).

View File

@ -10,8 +10,8 @@ This will launch a customized version of [the built-in Django shell](https://doc
``` ```
$ ./manage.py nbshell $ ./manage.py nbshell
### NetBox interactive shell (jstretch-laptop) ### NetBox interactive shell (localhost)
### Python 3.5.2 | Django 2.0.8 | NetBox 2.4.3 ### Python 3.6.9 | Django 2.2.11 | NetBox 2.7.10
### lsmodels() will show available models. Use help(<model>) for more info. ### lsmodels() will show available models. Use help(<model>) for more info.
``` ```

View File

@ -2,18 +2,7 @@
The NetBox API employs token-based authentication. For convenience, cookie authentication can also be used when navigating the browsable API. The NetBox API employs token-based authentication. For convenience, cookie authentication can also be used when navigating the browsable API.
## Tokens {!docs/models/users/token.md!}
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.
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
## Authenticating to the API ## Authenticating to the API

View File

@ -17,7 +17,7 @@ E.g. filtering based on a device's name:
While you are able to filter based on an arbitrary number of fields, you are also able to While you are able to filter based on an arbitrary number of fields, you are also able to
pass multiple values for the same field. In most cases filtering on multiple values is pass multiple values for the same field. In most cases filtering on multiple values is
implemented as a logical OR operation. A notible exception is the `tag` filter which implemented as a logical OR operation. A notable exception is the `tag` filter which
is a logical AND. Passing multiple values for one field, can be combined with other fields. is a logical AND. Passing multiple values for one field, can be combined with other fields.
For example, filtering for devices with either the name of DC-SPINE-1 _or_ DC-LEAF-4: For example, filtering for devices with either the name of DC-SPINE-1 _or_ DC-LEAF-4:
@ -33,11 +33,11 @@ _both_ of those tags applied:
## Lookup Expressions ## Lookup Expressions
Certain model fields also support filtering using additonal lookup expressions. This allows Certain model fields also support filtering using additional lookup expressions. This allows
for negation and other context specific filtering. for negation and other context specific filtering.
These lookup expressions can be applied by adding a suffix to the desired field's name. These lookup expressions can be applied by adding a suffix to the desired field's name.
E.g. `mac_address__n`. In this case, the filter expression is for negation and it is seperated E.g. `mac_address__n`. In this case, the filter expression is for negation and it is separated
by two underscores. Below are the lookup expressions that are supported across different field by two underscores. Below are the lookup expressions that are supported across different field
types. types.

View File

@ -187,37 +187,6 @@ GET /api/ipam/prefixes/13980/?brief=1
The brief format is supported for both lists and individual objects. The brief format is supported for both lists and individual objects.
### Static Choice Fields
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.
Each choice includes a human-friendly label and its corresponding numeric value. For example, `GET /api/ipam/_choices/prefix:status/` will return:
```
[
{
"value": 0,
"label": "Container"
},
{
"value": 1,
"label": "Active"
},
{
"value": 2,
"label": "Reserved"
},
{
"value": 3,
"label": "Deprecated"
}
]
```
Thus, to set a prefix's status to "Reserved," it would be assigned the integer `2`.
A request for `GET /api/ipam/_choices/` will return choices for _all_ fields belonging to models within the IPAM app.
## Pagination ## Pagination
API responses which contain a list of objects (for example, a request to `/api/dcim/devices/`) will be paginated to avoid unnecessary overhead. The root JSON object will contain the following attributes: API responses which contain a list of objects (for example, a request to `/api/dcim/devices/`) will be paginated to avoid unnecessary overhead. The root JSON object will contain the following attributes:
@ -274,33 +243,38 @@ The maximum number of objects that can be returned is limited by the [`MAX_PAGE_
## Filtering ## Filtering
A list of objects retrieved via the API can be filtered by passing one or more query parameters. The same parameters used by the web UI work for the API as well. For example, to return only prefixes with a status of "Active" (`1`): A list of objects retrieved via the API can be filtered by passing one or more query parameters. The same parameters used by the web UI work for the API as well. For example, to return only prefixes with a status of "Active" (identified by the slug `active`):
``` ```
GET /api/ipam/prefixes/?status=1 GET /api/ipam/prefixes/?status=active
``` ```
The choices available for fixed choice fields such as `status` are exposed in the API under a special `_choices` endpoint for each NetBox app. For example, the available choices for `Prefix.status` are listed at `/api/ipam/_choices/` under the key `prefix:status`: The choices available for fixed choice fields such as `status` can be retrieved by sending an `OPTIONS` API request for the desired endpoint:
``` ```no-highlight
"prefix:status": [ $ curl -s -X OPTIONS \
{ -H "Authorization: Token $TOKEN" \
"label": "Container", -H "Content-Type: application/json" \
"value": 0 -H "Accept: application/json; indent=4" \
}, http://localhost:8000/api/ipam/prefixes/ | jq ".actions.POST.status.choices"
{ [
"label": "Active", {
"value": 1 "value": "container",
}, "display_name": "Container"
{ },
"label": "Reserved", {
"value": 2 "value": "active",
}, "display_name": "Active"
{ },
"label": "Deprecated", {
"value": 3 "value": "reserved",
} "display_name": "Reserved"
], },
{
"value": "deprecated",
"display_name": "Deprecated"
}
]
``` ```
For most fields, when a filter is passed multiple times, objects matching _any_ of the provided values will be returned. For example, `GET /api/dcim/sites/?name=Foo&name=Bar` will return all sites named "Foo" _or_ "Bar". The exception to this rule is ManyToManyFields which may have multiple values assigned. Tags are the most common example of a ManyToManyField. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites tagged with both "foo" _and_ "bar". For most fields, when a filter is passed multiple times, objects matching _any_ of the provided values will be returned. For example, `GET /api/dcim/sites/?name=Foo&name=Bar` will return all sites named "Foo" _or_ "Bar". The exception to this rule is ManyToManyFields which may have multiple values assigned. Tags are the most common example of a ManyToManyField. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites tagged with both "foo" _and_ "bar".

View File

@ -86,7 +86,12 @@ CORS_ORIGIN_WHITELIST = [
Default: False Default: False
This setting enables debugging. This should be done only during development or troubleshooting. Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users. This setting enables debugging. This should be done only during development or troubleshooting. Note that only clients
which access NetBox from a recognized [internal IP address](#internal_ips) will see debugging tools in the user
interface.
!!! warning
Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users.
--- ---
@ -108,16 +113,20 @@ The file path to NetBox's documentation. This is used when presenting context-se
## EMAIL ## EMAIL
In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` setting: In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` configuration parameter:
* SERVER - Host name or IP address of the email server (use `localhost` if running locally) * `SERVER` - Host name or IP address of the email server (use `localhost` if running locally)
* PORT - TCP port to use for the connection (default: 25) * `PORT` - TCP port to use for the connection (default: `25`)
* USERNAME - Username with which to authenticate * `USERNAME` - Username with which to authenticate
* PASSSWORD - Password with which to authenticate * `PASSSWORD` - Password with which to authenticate
* TIMEOUT - Amount of time to wait for a connection (seconds) * `USE_SSL` - Use SSL when connecting to the server (default: `False`). Mutually exclusive with `USE_TLS`.
* FROM_EMAIL - Sender address for emails sent by NetBox * `USE_TLS` - Use TLS when connecting to the server (default: `False`). Mutually exclusive with `USE_SSL`.
* `SSL_CERTFILE` - Path to the PEM-formatted SSL certificate file (optional)
* `SSL_KEYFILE` - Path to the PEM-formatted SSL private key file (optional)
* `TIMEOUT` - Amount of time to wait for a connection, in seconds (default: `10`)
* `FROM_EMAIL` - Sender address for emails sent by NetBox (default: `root@localhost`)
Email is sent from NetBox only for critical events. If you would like to test the email server configuration please use the django function [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail): Email is sent from NetBox only for critical events or if configured for [logging](#logging). If you would like to test the email server configuration please use the django function [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail):
``` ```
# python ./manage.py nbshell # python ./manage.py nbshell
@ -165,6 +174,31 @@ Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce uni
--- ---
## HTTP_PROXIES
Default: None
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhooks). Proxies should be specified by schema as per the [Python requests library documentation](https://2.python-requests.org/en/master/user/advanced/). For example:
```python
HTTP_PROXIES = {
'http': 'http://10.10.1.10:3128',
'https': 'http://10.10.1.10:1080',
}
```
---
## INTERNAL_IPS
Default: `('127.0.0.1', '::1',)`
A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
addresses (and [`DEBUG`](#debug) is true).
---
## LOGGING ## LOGGING
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`. By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
@ -191,6 +225,14 @@ LOGGING = {
} }
``` ```
### Available Loggers
* `netbox.auth.*` - Authentication events
* `netbox.api.views.*` - Views which handle business logic for the REST API
* `netbox.reports.*` - Report execution (`module.name`)
* `netbox.scripts.*` - Custom script execution (`module.name`)
* `netbox.views.*` - Views which handle business logic for the web UI
--- ---
## LOGIN_REQUIRED ## LOGIN_REQUIRED
@ -291,6 +333,39 @@ Determine how many objects to display per page within each list of objects.
--- ---
## PLUGINS
Default: Empty
A list of installed [NetBox plugins](../../plugins/) to enable. Plugins will not take effect unless they are listed here.
!!! warning
Plugins extend NetBox by allowing external code to run with the same access and privileges as NetBox itself. Only install plugins from trusted sources. The NetBox maintainers make absolutely no guarantees about the integrity or security of your installation with plugins enabled.
---
## PLUGINS_CONFIG
Default: Empty
This parameter holds configuration settings for individual NetBox plugins. It is defined as a dictionary, with each key using the name of an installed plugin. The specific parameters supported are unique to each plugin: Reference the plugin's documentation to determine the supported parameters. An example configuration is shown below:
```python
PLUGINS_CONFIG = {
'plugin1': {
'foo': 123,
'bar': True
},
'plugin2': {
'foo': 456,
},
}
```
Note that a plugin must be listed in `PLUGINS` for its configuration to take effect.
---
## PREFER_IPV4 ## PREFER_IPV4
Default: False Default: False
@ -299,6 +374,72 @@ When determining the primary IP address for a device, IPv6 is preferred over IPv
--- ---
## REMOTE_AUTH_ENABLED
Default: `False`
NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.)
---
## REMOTE_AUTH_BACKEND
Default: `'utilities.auth_backends.RemoteUserBackend'`
Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication, if not using NetBox's built-in backend. (Requires `REMOTE_AUTH_ENABLED`.)
---
## REMOTE_AUTH_HEADER
Default: `'HTTP_REMOTE_USER'`
When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. (Requires `REMOTE_AUTH_ENABLED`.)
---
## REMOTE_AUTH_AUTO_CREATE_USER
Default: `False`
If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
---
## REMOTE_AUTH_DEFAULT_GROUPS
Default: `[]` (Empty list)
The list of groups to assign a new user account when created using remote authentication. (Requires `REMOTE_AUTH_ENABLED`.)
---
## REMOTE_AUTH_DEFAULT_PERMISSIONS
Default: `[]` (Empty list)
The list of permissions to assign a new user account when created using remote authentication. (Requires `REMOTE_AUTH_ENABLED`.)
---
## RELEASE_CHECK_TIMEOUT
Default: 86,400 (24 hours)
The number of seconds to retain the latest version that is fetched from the GitHub API before automatically invalidating it and fetching it from the API again. This must be set to at least one hour (3600 seconds).
---
## RELEASE_CHECK_URL
Default: None
The releases of this repository are checked to detect new releases, which are shown on the home page of the web interface. You can change this to your own fork of the NetBox repository, or set it to `None` to disable the check. The URL provided **must** be compatible with the GitHub API.
Use `'https://api.github.com/repos/netbox-community/netbox/releases'` to check for release in the official NetBox repository.
---
## REPORTS_ROOT ## REPORTS_ROOT
Default: $BASE_DIR/netbox/reports/ Default: $BASE_DIR/netbox/reports/

View File

@ -46,9 +46,9 @@ DATABASE = {
[Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of [Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of
NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching
functionality (as well as other planned features). In 2.7, the connection settings were broken down into two sections for functionality (as well as other planned features). In 2.7, the connection settings were broken down into two sections for
webhooks and caching, allowing the user to connect to different Redis instances/databases per feature. task queuing and caching, allowing the user to connect to different Redis instances/databases per feature.
Redis is configured using a configuration setting similar to `DATABASE` and these settings are the same for both of the `webhooks` and `caching` subsections: Redis is configured using a configuration setting similar to `DATABASE` and these settings are the same for both of the `tasks` and `caching` subsections:
* `HOST` - Name or IP address of the Redis server (use `localhost` if running locally) * `HOST` - Name or IP address of the Redis server (use `localhost` if running locally)
* `PORT` - TCP port of the Redis service; leave blank for default port (6379) * `PORT` - TCP port of the Redis service; leave blank for default port (6379)
@ -61,7 +61,7 @@ Example:
```python ```python
REDIS = { REDIS = {
'webhooks': { 'tasks': {
'HOST': 'redis.example.com', 'HOST': 'redis.example.com',
'PORT': 1234, 'PORT': 1234,
'PASSWORD': 'foobar', 'PASSWORD': 'foobar',
@ -84,9 +84,9 @@ REDIS = {
If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have
changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary
!!! note !!! warning
It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the It is highly recommended to keep the task and cache databases separate. Using the same database number on the
same Redis instance for both may result in webhook processing data being lost during cache flushing events. same Redis instance for both may result in queued background tasks being lost during cache flushing events.
### Using Redis Sentinel ### Using Redis Sentinel
@ -102,7 +102,7 @@ Example:
```python ```python
REDIS = { REDIS = {
'webhooks': { 'tasks': {
'SENTINELS': [('mysentinel.redis.example.com', 6379)], 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
'SENTINEL_SERVICE': 'netbox', 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': '', 'PASSWORD': '',
@ -126,7 +126,7 @@ REDIS = {
!!! note !!! note
It is possible to have only one or the other Redis configurations to use Sentinel functionality. It is possible It is possible to have only one or the other Redis configurations to use Sentinel functionality. It is possible
for example to have the webhook use sentinel via `HOST`/`PORT` and for caching to use Sentinel via for example to have the tasks database use sentinel via `HOST`/`PORT` and for caching to use Sentinel via
`SENTINELS`/`SENTINEL_SERVICE`. `SENTINELS`/`SENTINEL_SERVICE`.

View File

@ -0,0 +1,55 @@
# Application Registry
The registry is an in-memory data structure which houses various miscellaneous application-wide parameters, such as installed plugins. It is not exposed to the user and is not intended to be modified by any code outside of NetBox core.
The registry behaves essentially like a Python dictionary, with the notable exception that once a store (key) has been declared, it cannot be deleted or overwritten. The value of a store can, however, me modified; e.g. by appending a value to a list. Store values generally do not change once the application has been initialized.
## Stores
### `model_features`
A dictionary of particular features (e.g. custom fields) mapped to the NetBox models which support them, arranged by app. For example:
```python
{
'custom_fields': {
'circuits': ['provider', 'circuit'],
'dcim': ['site', 'rack', 'devicetype', ...],
...
},
'webhooks': {
...
},
...
}
```
### `plugin_menu_items`
Navigation menu items provided by NetBox plugins. Each plugin is registered as a key with the list of menu items it provides. An example:
```python
{
'Plugin A': (
<MenuItem>, <MenuItem>, <MenuItem>,
),
'Plugin B': (
<MenuItem>, <MenuItem>, <MenuItem>,
),
}
```
### `plugin_template_extensions`
Plugin content that gets embedded into core NetBox templates. The store comprises NetBox models registered as dictionary keys, each pointing to a list of applicable template extension classes that exist. An example:
```python
{
'dcim.site': [
<TemplateExtension>, <TemplateExtension>, <TemplateExtension>,
],
'dcim.rack': [
<TemplateExtension>, <TemplateExtension>,
],
}
```

View File

@ -35,13 +35,9 @@ Update the following static libraries to their most recent stable release:
* jQuery * jQuery
* jQuery UI * jQuery UI
### Squash Schema Migrations
Database schema migrations should be squashed for each new minor release. See the [squashing guide](squashing-migrations.md) for the detailed process.
### Create a new Release Notes Page ### Create a new Release Notes Page
Create a file at `/docs/release-notes/X.Y.md` to establish the release notes for the new release. Add the file to the table of contents within `mkdocs.yml`. Create a file at `/docs/release-notes/X.Y.md` to establish the release notes for the new release. Add the file to the table of contents within `mkdocs.yml`, and point `index.md` to the new file.
### Manually Perform a New Install ### Manually Perform a New Install

View File

@ -1,168 +0,0 @@
# Squashing Database Schema Migrations
## What are Squashed Migrations?
The Django framework on which NetBox is built utilizes [migration files](https://docs.djangoproject.com/en/stable/topics/migrations/) to keep track of changes to the PostgreSQL database schema. Each time a model is altered, the resulting schema change is captured in a migration file, which can then be applied to effect the new schema.
As changes are made over time, more and more migration files are created. Although not necessarily problematic, it can be beneficial to merge and compress these files occasionally to reduce the total number of migrations that need to be applied upon installation of NetBox. This merging process is called _squashing_ in Django vernacular, and results in two parallel migration paths: individual and squashed.
Below is an example showing both individual and squashed migration files within an app:
| Individual | Squashed |
|------------|----------|
| 0001_initial | 0001_initial_squashed_0004_add_field |
| 0002_alter_field | . |
| 0003_remove_field | . |
| 0004_add_field | . |
| 0005_another_field | 0005_another_field |
In the example above, a new installation can leverage the squashed migrations to apply only two migrations:
* `0001_initial_squashed_0004_add_field`
* `0005_another_field`
This is because the squash file contains all of the operations performed by files `0001` through `0004`.
However, an existing installation that has already applied some of the individual migrations contained within the squash file must continue applying individual migrations. For instance, an installation which currently has up to `0002_alter_field` applied must apply the following migrations to become current:
* `0003_remove_field`
* `0004_add_field`
* `0005_another_field`
Squashed migrations are opportunistic: They are used only if applicable to the current environment. Django will fall back to using individual migrations if the squashed migrations do not agree with the current database schema at any point.
## Squashing Migrations
During every minor (i.e. 2.x) release, migrations should be squashed to help simplify the migration process for new installations. The process below describes how to squash migrations efficiently and with minimal room for error.
### 1. Create a New Branch
Create a new branch off of the `develop-2.x` branch. (Migrations should be squashed _only_ in preparation for a new minor release.)
```
git checkout -B squash-migrations
```
### 2. Delete Existing Squash Files
Delete the most recent squash file within each NetBox app. This allows us to extend squash files where the opportunity exists. For example, we might be able to replace `0005_to_0008` with `0005_to_0011`.
### 3. Generate the Current Migration Plan
Use Django's `showmigrations` utility to display the order in which all migrations would be applied for a new installation.
```
manage.py showmigrations --plan
```
From the resulting output, delete all lines which reference an external migration. Any migrations imposed by Django itself on an external package are not relevant.
### 4. Create Squash Files
Begin iterating through the migration plan, looking for successive sets of migrations within an app. These are candidates for squashing. For example:
```
[X] extras.0014_configcontexts
[X] extras.0015_remove_useraction
[X] extras.0016_exporttemplate_add_cable
[X] extras.0017_exporttemplate_mime_type_length
[ ] extras.0018_exporttemplate_add_jinja2
[ ] extras.0019_tag_taggeditem
[X] dcim.0062_interface_mtu
[X] dcim.0063_device_local_context_data
[X] dcim.0064_remove_platform_rpc_client
[ ] dcim.0065_front_rear_ports
[X] circuits.0001_initial_squashed_0010_circuit_status
[ ] dcim.0066_cables
...
```
Migrations `0014` through `0019` in `extras` can be squashed, as can migrations `0062` through `0065` in `dcim`. Migration `0066` cannot be included in the same squash file, because the `circuits` migration must be applied before it. (Note that whether or not each migration is currently applied to the database does not matter.)
Squash files are created using Django's `squashmigrations` utility:
```
manage.py squashmigrations <app> <start> <end>
```
For example, our first step in the example would be to run `manage.py squashmigrations extras 0014 0019`.
!!! note
Specifying a migration file's numeric index is enough to uniquely identify it within an app. There is no need to specify the full filename.
This will create a new squash file within the app's `migrations` directory, named as a concatenation of its beginning and ending migration. Some manual editing is necessary for each new squash file for housekeeping purposes:
* Remove the "automatically generated" comment at top (to indicate that a human has reviewed the file).
* Reorder `import` statements as necessary per PEP8.
* It may be necessary to copy over custom functions from the original migration files (this will be indicated by a comment near the top of the squash file). It is safe to remove any functions that exist solely to accomodate reverse migrations (which we no longer support).
Repeat this process for each candidate set of migrations until you reach the end of the migration plan.
### 5. Check for Missing Migrations
If everything went well, at this point we should have a completed squashed path. Perform a dry run to check for any missing migrations:
```
manage.py migrate --dry-run
```
### 5. Run Migrations
Next, we'll apply the entire migration path to an empty database. Begin by dropping and creating your development database.
!!! warning
Obviously, first back up any data you don't want to lose.
```
sudo -u postgres psql -c 'drop database netbox'
sudo -u postgres psql -c 'create database netbox'
```
Apply the migrations with the `migrate` management command. It is not necessary to specify a particular migration path; Django will detect and use the squashed migrations automatically. You can verify the exact migrations being applied by enabling verboes output with `-v 2`.
```
manage.py migrate -v 2
```
### 6. Commit the New Migrations
If everything is successful to this point, commit your changes to the `squash-migrations` branch.
### 7. Validate Resulting Schema
To ensure our new squashed migrations do not result in a deviation from the original schema, we'll compare the two. With the new migration file safely commit, check out the `develop-2.x` branch, which still contains only the individual migrations.
```
git checkout develop-2.x
```
Temporarily install the [django-extensions](https://django-extensions.readthedocs.io/) package, which provides the `sqldiff utility`:
```
pip install django-extensions
```
Also add `django_extensions` to `INSTALLED_APPS` in `netbox/netbox/settings.py`.
At this point, our database schema has been defined by using the squashed migrations. We can run `sqldiff` to see if it differs any from what the current (non-squashed) migrations would generate. `sqldiff` accepts a list of apps against which to run:
```
manage.py sqldiff circuits dcim extras ipam secrets tenancy users virtualization
```
It is safe to ignore errors indicating an "unknown database type" for the following fields:
* `dcim_interface.mac_address`
* `ipam_aggregate.prefix`
* `ipam_prefix.prefix`
It is also safe to ignore the message "Table missing: extras_script".
Resolve any differences by correcting migration files in the `squash-migrations` branch.
!!! warning
Don't forget to remove `django_extension` from `INSTALLED_APPS` before committing your changes.
### 8. Merge the Squashed Migrations
Once all squashed migrations have been validated and all tests run successfully, merge the `squash-migrations` branch into `develop-2.x`. This completes the squashing process.

View File

@ -0,0 +1,11 @@
# User Preferences
The `users.UserConfig` model holds individual preferences for each user in the form of JSON data. This page serves as a manifest of all recognized user preferences in NetBox.
## Available Preferences
| Name | Description |
| ---- | ----------- |
| extras.configcontext.format | Preferred format when rendering config context data (JSON or YAML) |
| pagination.per_page | The number of items to display per page of a paginated table |
| tables.${table_name}.columns | The ordered list of columns to display when viewing the table |

19
docs/extra.css Normal file
View File

@ -0,0 +1,19 @@
/* Images */
img {
display: block;
margin-left: auto;
margin-right: auto;
}
/* Tables */
table {
margin-bottom: 24px;
width: 100%;
}
th {
background-color: #f0f0f0;
padding: 6px;
}
td {
padding: 6px;
}

View File

@ -49,13 +49,13 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
| HTTP service | nginx or Apache | | HTTP service | nginx or Apache |
| WSGI service | gunicorn or uWSGI | | WSGI service | gunicorn or uWSGI |
| Application | Django/Python | | Application | Django/Python |
| Database | PostgreSQL 9.4+ | | Database | PostgreSQL 9.6+ |
| Task queuing | Redis/django-rq | | Task queuing | Redis/django-rq |
| Live device access | NAPALM | | Live device access | NAPALM |
## Supported Python Versions ## Supported Python Versions
NetBox supports Python 3.5, 3.6, and 3.7 environments currently. Python 3.5 is scheduled to be unsupported in NetBox v2.8. NetBox supports Python 3.6 and 3.7 environments currently. (Support for Python 3.5 was removed in NetBox v2.8.)
## Getting Started ## Getting Started

View File

@ -3,7 +3,7 @@
This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md). This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md).
!!! warning !!! warning
NetBox requires PostgreSQL 9.4 or higher. Please note that MySQL and other relational databases are **not** supported. NetBox requires PostgreSQL 9.6 or higher. Please note that MySQL and other relational databases are **not** supported.
The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
@ -20,10 +20,10 @@ If a recent enough version of PostgreSQL is not available through your distribut
#### CentOS #### CentOS
CentOS 7.5 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6. CentOS 7.5 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6, however you may opt to install a more recent version.
```no-highlight ```no-highlight
# yum install -y https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm # yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# yum install -y postgresql96 postgresql96-server postgresql96-devel # yum install -y postgresql96 postgresql96-server postgresql96-devel
# /usr/pgsql-9.6/bin/postgresql96-setup initdb # /usr/pgsql-9.6/bin/postgresql96-setup initdb
``` ```
@ -51,7 +51,7 @@ At a minimum, we need to create a database for NetBox and assign it a username a
```no-highlight ```no-highlight
# sudo -u postgres psql # sudo -u postgres psql
psql (9.4.5) psql (10.10)
Type "help" for help. Type "help" for help.
postgres=# CREATE DATABASE netbox; postgres=# CREATE DATABASE netbox;

View File

@ -1,13 +1,15 @@
# NetBox Installation # NetBox Installation
This section of the documentation discusses installing and configuring the NetBox application. Begin by installing all system packages required by NetBox and its dependencies: This section of the documentation discusses installing and configuring the NetBox application itself.
## Install System Packages ## Install System Packages
Begin by installing all system packages required by NetBox and its dependencies. Note that beginning with NetBox v2.8, Python 3.6 or later is required.
### Ubuntu ### Ubuntu
```no-highlight ```no-highlight
# apt-get install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev # apt-get install -y python3.6 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev
``` ```
### CentOS ### CentOS
@ -76,7 +78,8 @@ Create a system user account named `netbox`. We'll configure the WSGI and HTTP s
CentOS users may need to create the `netbox` group first. CentOS users may need to create the `netbox` group first.
``` ```
# adduser --system --group netbox # groupadd --system netbox
# adduser --system --gid netbox netbox
# chown --recursive netbox /opt/netbox/netbox/media/ # chown --recursive netbox /opt/netbox/netbox/media/
``` ```
@ -172,7 +175,7 @@ Redis is a in-memory key-value store required as part of the NetBox installation
```python ```python
REDIS = { REDIS = {
'webhooks': { 'tasks': {
'HOST': 'redis.example.com', 'HOST': 'redis.example.com',
'PORT': 1234, 'PORT': 1234,
'PASSWORD': 'foobar', 'PASSWORD': 'foobar',

View File

@ -5,6 +5,18 @@ We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for
!!! info !!! info
For the sake of brevity, only Ubuntu 18.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed. For the sake of brevity, only Ubuntu 18.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
## Obtain an SSL Certificate
To enable HTTPS access to NetBox, you'll need a valid SSL certificate. You can purchase one from a trusted commercial provider, obtain one for free from [Let's Encrypt](https://letsencrypt.org/getting-started/), or generate your own (although self-signed certificates are generally untrusted). Both the public certificate and private key files need to be installed on your NetBox server in a location that is readable by the `netbox` user.
The command below can be used to generate a self-signed certificate for testing purposes, however it is strongly recommended to use a certificate from a trusted authority in production. Two files will be created: the public certificate (`netbox.crt`) and the private key (`netbox.key`). The certificate is published to the world, whereas the private key must be kept secret at all times.
```no-highlight
# openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/netbox.key \
-out /etc/ssl/certs/netbox.crt
```
## HTTP Daemon Installation ## HTTP Daemon Installation
### Option A: nginx ### Option A: nginx
@ -15,27 +27,10 @@ The following will serve as a minimal nginx configuration. Be sure to modify you
# apt-get install -y nginx # apt-get install -y nginx
``` ```
Once nginx is installed, save the following configuration to `/etc/nginx/sites-available/netbox`. Be sure to replace `netbox.example.com` with the domain name or IP address of your installation. (This should match the value configured for `ALLOWED_HOSTS` in `configuration.py`.) Once nginx is installed, copy the default nginx configuration file to `/etc/nginx/sites-available/netbox`. Be sure to replace `netbox.example.com` with the domain name or IP address of your installation. (This should match the value configured for `ALLOWED_HOSTS` in `configuration.py`.)
```nginx ```no-highlight
server { # cp /opt/netbox/contrib/nginx.conf /etc/nginx/sites-available/netbox
listen 80;
server_name netbox.example.com;
client_max_body_size 25m;
location /static/ {
alias /opt/netbox/netbox/static/;
}
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
``` ```
Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created. Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created.
@ -46,65 +41,38 @@ Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sit
# ln -s /etc/nginx/sites-available/netbox # ln -s /etc/nginx/sites-available/netbox
``` ```
Restart the nginx service to use the new configuration. Finally, restart the `nginx` service to use the new configuration.
```no-highlight ```no-highlight
# service nginx restart # service nginx restart
``` ```
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04).
### Option B: Apache ### Option B: Apache
```no-highlight Begin by installing Apache:
# 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):
```apache
<VirtualHost *:80>
ProxyPreserveHost On
ServerName netbox.example.com
Alias /static /opt/netbox/netbox/static
# Needed to allow token-based API authentication
WSGIPassAuthorization on
<Directory /opt/netbox/netbox/static>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Require all granted
</Directory>
<Location /static>
ProxyPass !
</Location>
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
ProxyPass / http://127.0.0.1:8001/
ProxyPassReverse / http://127.0.0.1:8001/
</VirtualHost>
```
Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf`, enable the `proxy` and `proxy_http` modules, and reload Apache:
```no-highlight ```no-highlight
# a2enmod proxy # apt-get install -y apache2
# a2enmod proxy_http ```
# a2enmod headers
Next, copy the default configuration file to `/etc/apache2/sites-available/`. Be sure to modify the `ServerName` parameter appropriately.
```no-highlight
# cp /opt/netbox/contrib/apache.conf /etc/apache2/sites-available/netbox.conf
```
Finally, ensure that the required Apache modules are enabled, enable the `netbox` site, and reload Apache:
```no-highlight
# a2enmod ssl proxy proxy_http headers
# a2ensite netbox # a2ensite netbox
# service apache2 restart # service apache2 restart
``` ```
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04).
!!! note !!! note
Certain components of NetBox (such as the display of rack elevation diagrams) rely on the use of embedded objects. Ensure that your HTTP server configuration does not override the `X-Frame-Options` response header set by NetBox. Certain components of NetBox (such as the display of rack elevation diagrams) rely on the use of embedded objects. Ensure that your HTTP server configuration does not override the `X-Frame-Options` response header set by NetBox.
## gunicorn Configuration ## Gunicorn Configuration
Copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade.) Copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade.)
@ -113,7 +81,7 @@ Copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a
# cp contrib/gunicorn.py /opt/netbox/gunicorn.py # cp contrib/gunicorn.py /opt/netbox/gunicorn.py
``` ```
You may wish to edit this file to change the bound IP address or port number, or to make performance-related adjustments. You may wish to edit this file to change the bound IP address or port number, or to make performance-related adjustments. See [the Gunicorn documentation](https://docs.gunicorn.org/en/stable/configure.html) for the available configuration parameters.
## systemd Configuration ## systemd Configuration
@ -133,7 +101,7 @@ Then, start the `netbox` and `netbox-rq` services and enable them to initiate at
You can use the command `systemctl status netbox` to verify that the WSGI service is running: You can use the command `systemctl status netbox` to verify that the WSGI service is running:
``` ```no-highlight
# systemctl status netbox.service # systemctl status netbox.service
● netbox.service - NetBox WSGI Service ● netbox.service - NetBox WSGI Service
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled) Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)

View File

@ -135,7 +135,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
## Troubleshooting LDAP ## Troubleshooting LDAP
`supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`. `systemctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`. For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`.

View File

@ -8,6 +8,10 @@ The following sections detail how to set up a new instance of NetBox:
4. [HTTP daemon](4-http-daemon.md) 4. [HTTP daemon](4-http-daemon.md)
5. [LDAP authentication](5-ldap.md) (optional) 5. [LDAP authentication](5-ldap.md) (optional)
Below is a simplified overview of the NetBox application stack for reference:
![NetBox UI as seen by a non-authenticated user](../media/installation/netbox_application_stack.png)
## Upgrading ## Upgrading
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md). If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).

View File

@ -7,7 +7,7 @@ This document contains instructions for migrating from a legacy NetBox deploymen
### Uninstall supervisord ### Uninstall supervisord
```no-highlight ```no-highlight
# apt-get remove -y supervisord # apt-get remove -y supervisor
``` ```
### Configure systemd ### Configure systemd

View File

@ -4,6 +4,9 @@
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../../release-notes/) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the version in which the change went into effect. Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../../release-notes/) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the version in which the change went into effect.
!!! note
Beginning with version 2.8, NetBox requires Python 3.6 or later.
## Install the Latest Code ## Install the Latest Code
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository. As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -2,6 +2,6 @@
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room. Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
Each rack group must be assigned to a parent site. Hierarchical recursion of rack groups is not currently supported. Each rack group must be assigned to a parent site, and rack groups may optionally be nested to achieve a multi-level hierarchy.
The name and facility ID of each rack within a group must be unique. (Racks not assigned to the same rack group may have identical names and/or facility IDs.) The name and facility ID of each rack within a group must be unique. (Racks not assigned to the same rack group may have identical names and/or facility IDs.)

View File

@ -1,3 +1,5 @@
# Tenant Groups # Tenant Groups
Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional. Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional.
Tenant groups may be nested to achieve a multi-level hierarchy. For example, you might have a group called "Customers" containing subgroups of individual tenants grouped by product or account team.

View File

@ -0,0 +1,12 @@
## Tokens
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.
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.

392
docs/plugins/development.md Normal file
View File

@ -0,0 +1,392 @@
# Plugin Development
This documentation covers the development of custom plugins for NetBox. Plugins are essentially self-contained [Django apps](https://docs.djangoproject.com/en/stable/) which integrate with NetBox to provide custom functionality. Since the development of Django apps is already very well-documented, we'll only be covering the aspects that are specific to NetBox.
Plugins can do a lot, including:
* Create Django models to store data in the database
* Provide their own "pages" (views) in the web user interface
* Inject template content and navigation links
* Establish their own REST API endpoints
* Add custom request/response middleware
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
## Initial Setup
## Plugin Structure
Although the specific structure of a plugin is largely left to the discretion of its authors, a typical NetBox plugin looks something like this:
```no-highlight
plugin_name/
- plugin_name/
- templates/
- plugin_name/
- *.html
- __init__.py
- middleware.py
- navigation.py
- signals.py
- template_content.py
- urls.py
- views.py
- README
- setup.py
```
The top level is the project root. Immediately within the root should exist several items:
* `setup.py` - This is a standard installation script used to install the plugin package within the Python environment.
* `README` - A brief introduction to your plugin, how to install and configure it, where to find help, and any other pertinent information. It is recommended to write README files using a markup language such as Markdown.
* The plugin source directory, with the same name as your plugin.
The plugin source directory contains all of the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class.
### Create setup.py
`setup.py` is the [setup script](https://docs.python.org/3.6/distutils/setupscript.html) we'll use to install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to inform the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
```python
from setuptools import find_packages, setup
setup(
name='netbox-animal-sounds',
version='0.1',
description='An example NetBox plugin',
url='https://github.com/netbox-community/netbox-animal-sounds',
author='Jeremy Stretch',
license='Apache 2.0',
install_requires=[],
packages=find_packages(),
include_package_data=True,
)
```
Many of these are self-explanatory, but for more information, see the [setuptools documentation](https://setuptools.readthedocs.io/en/latest/setuptools.html).
### Define a PluginConfig
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
```python
from extras.plugins import PluginConfig
class AnimalSoundsConfig(PluginConfig):
name = 'netbox_animal_sounds'
verbose_name = 'Animal Sounds'
description = 'An example plugin for development purposes'
version = '0.1'
author = 'Jeremy Stretch'
author_email = 'author@example.com'
base_url = 'animal-sounds'
required_settings = []
default_settings = {
'loud': False
}
config = AnimalSoundsConfig
```
NetBox looks for the `config` variable within a plugin's `__init__.py` to load its configuration. Typically, this will be set to the PluginConfig subclass, but you may wish to dynamically generate a PluginConfig class based on environment variables or other factors.
#### PluginConfig Attributes
| Name | Description |
| ---- | ----------- |
| `name` | Raw plugin name; same as the plugin's source directory |
| `verbose_name` | Human-friendly name for the plugin |
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
| `description` | Brief description of the plugin's purpose |
| `author` | Name of plugin's author |
| `author_email` | Author's public email address |
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values |
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `caching_config` | Plugin-specific cache configuration
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
### Install the Plugin for Development
To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `setup.py` from the plugin's root directory with the `develop` argument (instead of `install`):
```no-highlight
$ python setup.py develop
```
## Database Models
If your plugin introduces a new type of object in NetBox, you'll probably want to create a [Django model](https://docs.djangoproject.com/en/stable/topics/db/models/) for it. A model is essentially a Python representation of a database table, with attributes that represent individual columns. Model instances can be created, manipulated, and deleted using [queries](https://docs.djangoproject.com/en/stable/topics/db/queries/). Models must be defined within a file named `models.py`.
Below is an example `models.py` file containing a model with two character fields:
```python
from django.db import models
class Animal(models.Model):
name = models.CharField(max_length=50)
sound = models.CharField(max_length=50)
def __str__(self):
return self.name
```
Once you have defined the model(s) for your plugin, you'll need to create the database schema migrations. A migration file is essentially a set of instructions for manipulating the PostgreSQL database to support your new model, or to alter existing models. Creating migrations can usually be done automatically using Django's `makemigrations` management command.
!!! note
A plugin must be installed before it can be used with Django management commands. If you skipped this step above, run `python setup.py develop` from the plugin's root directory.
```no-highlight
$ ./manage.py makemigrations netbox_animal_sounds
Migrations for 'netbox_animal_sounds':
/home/jstretch/animal_sounds/netbox_animal_sounds/migrations/0001_initial.py
- Create model Animal
```
Next, we can apply the migration to the database with the `migrate` command:
```no-highlight
$ ./manage.py migrate netbox_animal_sounds
Operations to perform:
Apply all migrations: netbox_animal_sounds
Running migrations:
Applying netbox_animal_sounds.0001_initial... OK
```
For more background on schema migrations, see the [Django documentation](https://docs.djangoproject.com/en/stable/topics/migrations/).
### Using the Django Admin Interface
Plugins can optionally expose their models via Django's built-in [administrative interface](https://docs.djangoproject.com/en/stable/ref/contrib/admin/). This can greatly improve troubleshooting ability, particularly during development. To expose a model, simply register it using Django's `admin.register()` function. An example `admin.py` file for the above model is shown below:
```python
from django.contrib import admin
from .models import Animal
@admin.register(Animal)
class AnimalAdmin(admin.ModelAdmin):
list_display = ('name', 'sound')
```
This will display the plugin and its model in the admin UI. Staff users can create, change, and delete model instances via the admin UI without needing to create a custom view.
![NetBox plugin in the admin UI](../media/plugins/plugin_admin_ui.png)
## Views
If your plugin needs its own page or pages in the NetBox web UI, you'll need to define views. A view is a particular page tied to a URL within NetBox, which renders content using a template. Views are typically defined in `views.py`, and URL patterns in `urls.py`. As an example, let's write a view which displays a random animal and the sound it makes. First, we'll create the view in `views.py`:
```python
from django.shortcuts import render
from django.views.generic import View
from .models import Animal
class RandomAnimalView(View):
"""
Display a randomly-selected animal.
"""
def get(self, request):
animal = Animal.objects.order_by('?').first()
return render(request, 'netbox_animal_sounds/animal.html', {
'animal': animal,
})
```
This view retrieves a random animal from the database and and passes it as a context variable when rendering a template named `animal.html`, which doesn't exist yet. To create this template, first create a directory named `templates/netbox_animal_sounds/` within the plugin source directory. (We use the plugin's name as a subdirectory to guard against naming collisions with other plugins.) Then, create `animal.html`:
```jinja2
{% extends 'base.html' %}
{% block content %}
{% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
<h2 class="text-center" style="margin-top: 200px">
{% if animal %}
The {{ animal.name|lower }} says
{% if config.loud %}
{{ animal.sound|upper }}!
{% else %}
{{ animal.sound }}
{% endif %}
{% else %}
No animals have been created yet!
{% endif %}
</h2>
{% endwith %}
{% endblock %}
```
The first line of the template instructs Django to extend the NetBox base template and inject our custom content within its `content` block.
!!! note
Django renders templates with its own custom [template language](https://docs.djangoproject.com/en/stable/topics/templates/#the-django-template-language). This is very similar to Jinja2, however there are some important differences to be aware of.
Finally, to make the view accessible to users, we need to register a URL for it. We do this in `urls.py` by defining a `urlpatterns` variable containing a list of paths.
```python
from django.urls import path
from . import views
urlpatterns = [
path('random/', views.RandomAnimalView.as_view(), name='random_animal'),
]
```
A URL pattern has three components:
* `route` - The unique portion of the URL dedicated to this view
* `view` - The view itself
* `name` - A short name used to identify the URL path internally
This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it.
## REST API Endpoints
Plugins can declare custom endpoints on NetBox's REST API to retrieve or manipulate models or other data. These behave very similarly to views, except that instead of rendering arbitrary content using a template, data is returned in JSON format using a serializer. NetBox uses the [Django REST Framework](https://www.django-rest-framework.org/), which makes writing API serializers and views very simple.
First, we'll create a serializer for our `Animal` model, in `api/serializers.py`:
```python
from rest_framework.serializers import ModelSerializer
from netbox_animal_sounds.models import Animal
class AnimalSerializer(ModelSerializer):
class Meta:
model = Animal
fields = ('id', 'name', 'sound')
```
Next, we'll create a generic API view set that allows basic CRUD (create, read, update, and delete) operations for Animal instances. This is defined in `api/views.py`:
```python
from rest_framework.viewsets import ModelViewSet
from netbox_animal_sounds.models import Animal
from .serializers import AnimalSerializer
class AnimalViewSet(ModelViewSet):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
```
Finally, we'll register a URL for our endpoint in `api/urls.py`. This file **must** define a variable named `urlpatterns`.
```python
from rest_framework import routers
from .views import AnimalViewSet
router = routers.DefaultRouter()
router.register('animals', AnimalViewSet)
urlpatterns = router.urls
```
With these three components in place, we can request `/api/plugins/animal-sounds/animals/` to retrieve a list of all Animal objects defined.
![NetBox REST API plugin endpoint](../media/plugins/plugin_rest_api_endpoint.png)
!!! warning
This example is provided as a minimal reference implementation only. It does not address authentication, performance, or myriad other concerns that plugin authors should have.
## Navigation Menu Items
To make its views easily accessible to users, a plugin can inject items in NetBox's navigation menu under the "Plugins" header. Menu items are added by defining a list of PluginMenuItem instances. By default, this should be a variable named `menu_items` in the file `navigation.py`. An example is shown below.
```python
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
menu_items = (
PluginMenuItem(
link='plugins:netbox_animal_sounds:random_animal',
link_text='Random sound',
buttons=(
PluginMenuButton('home', 'Button A', 'fa fa-info', ButtonColorChoices.BLUE),
PluginMenuButton('home', 'Button B', 'fa fa-warning', ButtonColorChoices.GREEN),
)
),
)
```
A `PluginMenuItem` has the following attributes:
* `link` - The name of the URL path to which this menu item links
* `link_text` - The text presented to the user
* `permissions` - A list of permissions required to display this link (optional)
* `buttons` - An iterable of PluginMenuButton instances to display (optional)
A `PluginMenuButton` has the following attributes:
* `link` - The name of the URL path to which this button links
* `title` - The tooltip text (displayed when the mouse hovers over the button)
* `icon_class` - Button icon CSS class (NetBox currently supports [Font Awesome 4.7](https://fontawesome.com/v4.7.0/icons/))
* `color` - One of the choices provided by `ButtonColorChoices` (optional)
* `permissions` - A list of permissions required to display this button (optional)
## Extending Core Templates
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
* `left_page()` - Inject content on the left side of the page
* `right_page()` - Inject content on the right side of the page
* `full_width_page()` - Inject content across the entire bottom of the page
* `buttons()` - Add buttons to the top of the page
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:
* `object` - The object being viewed
* `request` - The current request
* `settings` - Global NetBox settings
* `config` - Plugin-specific configuration parameters
For example, accessing `{{ request.user }}` within a template will return the current user.
Declared subclasses should be gathered into a list or tuple for integration with NetBox. By default, NetBox looks for an iterable named `template_extensions` within a `template_content.py` file. (This can be overridden by setting `template_extensions` to a custom value on the plugin's PluginConfig.) An example is below.
```python
from extras.plugins import PluginTemplateExtension
from .models import Animal
class SiteAnimalCount(PluginTemplateExtension):
model = 'dcim.site'
def right_page(self):
return self.render('netbox_animal_sounds/inc/animal_count.html', extra_context={
'animal_count': Animal.objects.count(),
})
template_extensions = [SiteAnimalCount]
```
## Caching Configuration
By default, all query operations within a plugin are cached. To change this, define a caching configuration under the PluginConfig class' `caching_config` attribute. All configuration keys will be applied within the context of the plugin; there is no need to include the plugin name. An example configuration is below:
```python
class MyPluginConfig(PluginConfig):
...
caching_config = {
'foo': {
'ops': 'get',
'timeout': 60 * 15,
},
'*': {
'ops': 'all',
}
}
```
To disable caching for your plugin entirely, set:
```python
caching_config = {
'*': None
}
```
See the [django-cacheops](https://github.com/Suor/django-cacheops) documentation for more detail on configuring caching.

82
docs/plugins/index.md Normal file
View File

@ -0,0 +1,82 @@
# Plugins
Plugins are packaged [Django](https://docs.djangoproject.com/) apps that can be installed alongside NetBox to provide custom functionality not present in the core application. Plugins can introduce their own models and views, but cannot interfere with existing components. A NetBox user may opt to install plugins provided by the community or build his or her own.
Plugins are supported on NetBox v2.8 and later.
## Capabilities
The NetBox plugin architecture allows for the following:
* **Add new data models.** A plugin can introduce one or more models to hold data. (A model is essentially a table in the SQL database.)
* **Add new URLs and views.** Plugins can register URLs under the `/plugins` root path to provide browsable views for users.
* **Add content to existing model templates.** A template content class can be used to inject custom HTML content within the view of a core NetBox model. This content can appear in the left side, right side, or bottom of the page.
* **Add navigation menu items.** Each plugin can register new links in the navigation menu. Each link may have a set of buttons for specific actions, similar to the built-in navigation items.
* **Add custom middleware.** Custom Django middleware can be registered by each plugin.
* **Declare configuration parameters.** Each plugin can define required, optional, and default configuration parameters within its unique namespace. Plug configuration parameter are defined by the user under `PLUGINS_CONFIG` in `configuration.py`.
* **Limit installation by NetBox version.** A plugin can specify a minimum and/or maximum NetBox version with which it is compatible.
## Limitations
Either by policy or by technical limitation, the interaction of plugins with NetBox core is restricted in certain ways. A plugin may not:
* **Modify core models.** Plugins may not alter, remove, or override core NetBox models in any way. This rule is in place to ensure the integrity of the core data model.
* **Register URLs outside the `/plugins` root.** All plugin URLs are restricted to this path to prevent path collisions with core or other plugins.
* **Override core templates.** Plugins can inject additional content where supported, but may not manipulate or remove core content.
* **Modify core settings.** A configuration registry is provided for plugins, however they cannot alter or delete the core configuration.
* **Disable core components.** Plugins are not permitted to disable or hide core NetBox components.
## Installing Plugins
The instructions below detail the process for installing and enabling a NetBox plugin.
### Install Package
Download and install the plugin package per its installation instructions. Plugins published via PyPI are typically installed using pip. Be sure to install the plugin within NetBox's virtual environment.
```no-highlight
$ source /opt/netbox/venv/bin/activate
(venv) $ pip install <package>
```
Alternatively, you may wish to install the plugin manually by running `python setup.py install`. If you are developing a plugin and want to install it only temporarily, run `python setup.py develop` instead.
### Enable the Plugin
In `configuration.py`, add the plugin's name to the `PLUGINS` list:
```python
PLUGINS = [
'plugin_name',
]
```
### Configure Plugin
If the plugin requires any configuration, define it in `configuration.py` under the `PLUGINS_CONFIG` parameter. The available configuration parameters should be detailed in the plugin's README file.
```no-highlight
PLUGINS_CONFIG = {
'plugin_name': {
'foo': 'bar',
'buzz': 'bazz'
}
}
```
### Collect Static Files
Plugins may package static files to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
```no-highlight
(venv) $ cd /opt/netbox/netbox/
(venv) $ python3 manage.py collectstatic
```
### Restart WSGI Service
Restart the WSGI service to load the new plugin:
```no-highlight
# sudo systemctl restart netbox
```

View File

@ -1 +1 @@
version-2.7.md version-2.8.md

View File

@ -1,5 +1,50 @@
# NetBox v2.7 Release Notes # NetBox v2.7 Release Notes
## v2.7.12 (2020-04-08)
### Enhancements
* [#3676](https://github.com/netbox-community/netbox/issues/3676) - Reference VRF by name rather than RD during IP/prefix import
* [#4147](https://github.com/netbox-community/netbox/issues/4147) - Use absolute URLs in rack elevation SVG renderings
* [#4448](https://github.com/netbox-community/netbox/issues/4448) - Allow connecting cables between two circuit terminations
* [#4460](https://github.com/netbox-community/netbox/issues/4460) - Add the `webhook_receiver` management command to assist in troubleshooting outgoing webhooks
### Bug Fixes
* [#4395](https://github.com/netbox-community/netbox/issues/4395) - Fix typing of count_ipaddresses on interface serializer
* [#4418](https://github.com/netbox-community/netbox/issues/4418) - Fail cleanly when trying to import multiple device types simultaneously
* [#4438](https://github.com/netbox-community/netbox/issues/4438) - Fix exception when disconnecting a cable from a power feed
* [#4439](https://github.com/netbox-community/netbox/issues/4439) - Tweak display of unset custom integer fields
* [#4449](https://github.com/netbox-community/netbox/issues/4449) - Fix reservation edit/delete button URLs on rack view
---
## v2.7.11 (2020-03-27)
### Enhancements
* [#738](https://github.com/netbox-community/netbox/issues/738) - Add ability to automatically check for new releases (must be enabled by setting `RELEASE_CHECK_URL`)
* [#4255](https://github.com/netbox-community/netbox/issues/4255) - Custom script object variables now utilize dynamic form widgets
* [#4309](https://github.com/netbox-community/netbox/issues/4309) - Add descriptive tooltip to custom fields on object views
* [#4369](https://github.com/netbox-community/netbox/issues/4369) - Add a dedicated view for rack reservations
* [#4380](https://github.com/netbox-community/netbox/issues/4380) - Enable webhooks for rack reservations
* [#4381](https://github.com/netbox-community/netbox/issues/4381) - Enable export templates for rack reservations
* [#4382](https://github.com/netbox-community/netbox/issues/4382) - Enable custom links for rack reservations
* [#4386](https://github.com/netbox-community/netbox/issues/4386) - Update admin links for Django RQ to reflect multiple queues
* [#4389](https://github.com/netbox-community/netbox/issues/4389) - Add a bulk edit view for device bays
* [#4404](https://github.com/netbox-community/netbox/issues/4404) - Add cable trace button for circuit terminations
### Bug Fixes
* [#2769](https://github.com/netbox-community/netbox/issues/2769) - Improve `prefix_length` validation on available-prefixes API
* [#3193](https://github.com/netbox-community/netbox/issues/3193) - Fix cable tracing across multiple rear ports
* [#4340](https://github.com/netbox-community/netbox/issues/4340) - Enforce unique constraints for device and virtual machine names in the API
* [#4343](https://github.com/netbox-community/netbox/issues/4343) - Fix Markdown support for tables
* [#4365](https://github.com/netbox-community/netbox/issues/4365) - Fix exception raised on IP address bulk add view
* [#4415](https://github.com/netbox-community/netbox/issues/4415) - Fix duplicate name validation on device model
---
## v2.7.10 (2020-03-10) ## v2.7.10 (2020-03-10)
**Note:** If your deployment requires any non-core Python packages (such as `napalm`, `django-storages`, or `django-auth-ldap`), list them in a file named `local_requirements.txt` in the NetBox root directory (alongside `requirements.txt`). This will ensure they are detected and re-installed by the upgrade script when the Python virtual environment is rebuilt. **Note:** If your deployment requires any non-core Python packages (such as `napalm`, `django-storages`, or `django-auth-ldap`), list them in a file named `local_requirements.txt` in the NetBox root directory (alongside `requirements.txt`). This will ensure they are detected and re-installed by the upgrade script when the Python virtual environment is rebuilt.

View File

@ -0,0 +1,180 @@
# NetBox v2.8
## v2.8.5 (2020-05-26)
**Note:** The minimum required version of PostgreSQL is now 9.6.
### Enhancements
* [#4650](https://github.com/netbox-community/netbox/issues/4650) - Expose `INTERNAL_IPS` configuration parameter
* [#4651](https://github.com/netbox-community/netbox/issues/4651) - Add `csrf_token` context for plugin templates
* [#4652](https://github.com/netbox-community/netbox/issues/4652) - Add permissions context for plugin templates
* [#4665](https://github.com/netbox-community/netbox/issues/4665) - Add NEMA L14 and L21 power port/outlet types
* [#4672](https://github.com/netbox-community/netbox/issues/4672) - Set default color for rack and devices roles
### Bug Fixes
* [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
* [#4525](https://github.com/netbox-community/netbox/issues/4525) - Allow passing initial data to custom script MultiObjectVar
* [#4644](https://github.com/netbox-community/netbox/issues/4644) - Fix ordering of services table by parent
* [#4646](https://github.com/netbox-community/netbox/issues/4646) - Correct UI link for reports with custom name
* [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
* [#4648](https://github.com/netbox-community/netbox/issues/4648) - Fix bulk CSV import of child devices
* [#4649](https://github.com/netbox-community/netbox/issues/4649) - Fix interface assignment for bulk-imported IP addresses
* [#4676](https://github.com/netbox-community/netbox/issues/4676) - Set default value of `REMOTE_AUTH_AUTO_CREATE_USER` as `False` in docs
* [#4684](https://github.com/netbox-community/netbox/issues/4684) - Respect `comments` field when importing device type in YAML/JSON format
---
## v2.8.4 (2020-05-13)
### Enhancements
* [#4632](https://github.com/netbox-community/netbox/issues/4632) - Extend email configuration parameters to support SSL/TLS
### Bug Fixes
* [#4598](https://github.com/netbox-community/netbox/issues/4598) - Display error message when invalid cable length is specified
* [#4604](https://github.com/netbox-community/netbox/issues/4604) - Multi-position rear ports may only be connected to other rear ports
* [#4607](https://github.com/netbox-community/netbox/issues/4607) - Missing Contextual help for API Tokens
* [#4613](https://github.com/netbox-community/netbox/issues/4613) - Fix tag assignment on config contexts (regression from #4527)
* [#4617](https://github.com/netbox-community/netbox/issues/4617) - Restore IP prefix depth notation in list view
* [#4629](https://github.com/netbox-community/netbox/issues/4629) - Replicate assigned interface when cloning IP addresses
* [#4633](https://github.com/netbox-community/netbox/issues/4633) - Bump django-rq to v2.3.2 to fix ImportError with rq 1.4.0
* [#4634](https://github.com/netbox-community/netbox/issues/4634) - Inventory Item List view exception caused by incorrect accessor definition
---
## v2.8.3 (2020-05-06)
### Bug Fixes
* [#4593](https://github.com/netbox-community/netbox/issues/4593) - Fix AttributeError exception when viewing object lists as a non-authenticated user
---
## v2.8.2 (2020-05-06)
### Enhancements
* [#492](https://github.com/netbox-community/netbox/issues/492) - Enable toggling and rearranging table columns
* [#3147](https://github.com/netbox-community/netbox/issues/3147) - Allow specifying related objects by arbitrary attribute during CSV import
* [#3064](https://github.com/netbox-community/netbox/issues/3064) - Include tags in object lists as a toggleable table column
* [#3294](https://github.com/netbox-community/netbox/issues/3294) - Implement mechanism for storing user preferences
* [#4421](https://github.com/netbox-community/netbox/issues/4421) - Retain user's preference for config context format
* [#4502](https://github.com/netbox-community/netbox/issues/4502) - Enable configuration of proxies for outbound HTTP requests
* [#4531](https://github.com/netbox-community/netbox/issues/4531) - Retain user's preference for page length
* [#4554](https://github.com/netbox-community/netbox/issues/4554) - Add ServerTech's HDOT Cx power outlet type
### Bug Fixes
* [#4527](https://github.com/netbox-community/netbox/issues/4527) - Fix assignment of certain tags to config contexts
* [#4545](https://github.com/netbox-community/netbox/issues/4545) - Removed all squashed schema migrations to allow direct upgrades from very old releases
* [#4548](https://github.com/netbox-community/netbox/issues/4548) - Fix tracing cables through a single RearPort
* [#4549](https://github.com/netbox-community/netbox/issues/4549) - Fix encoding unicode webhook body data
* [#4556](https://github.com/netbox-community/netbox/issues/4556) - Update form for adding devices to clusters
* [#4578](https://github.com/netbox-community/netbox/issues/4578) - Prevent setting 0U height on device type with racked instances
* [#4584](https://github.com/netbox-community/netbox/issues/4584) - Ensure consistent support for filtering objects by `id` across all REST API endpoints
* [#4588](https://github.com/netbox-community/netbox/issues/4588) - Restore ability to add/remove tags on services, virtual chassis in bulk
---
## v2.8.1 (2020-04-23)
### Notes
In accordance with the fix in [#4459](https://github.com/netbox-community/netbox/issues/4459), users that are experiencing invalid nested data with
regions, rack groups, or tenant groups can perform a one-time operation using the NetBox shell to rebuild the correct nested relationships after upgrading:
```text
$ python netbox/manage.py nbshell
### NetBox interactive shell (localhost)
### Python 3.6.4 | Django 3.0.5 | NetBox 2.8.1
### lsmodels() will show available models. Use help(<model>) for more info.
>>> Region.objects.rebuild()
>>> RackGroup.objects.rebuild()
>>> TenantGroup.objects.rebuild()
```
### Enhancements
* [#4464](https://github.com/netbox-community/netbox/issues/4464) - Add 21-inch rack width (ETSI)
### Bug Fixes
* [#2994](https://github.com/netbox-community/netbox/issues/2994) - Prevent modifying termination points of existing cable to ensure end-to-end path integrity
* [#3356](https://github.com/netbox-community/netbox/issues/3356) - Correct Swagger schema specification for the available prefixes/IPs API endpoints
* [#4139](https://github.com/netbox-community/netbox/issues/4139) - Enable assigning all relevant attributes during bulk device/VM component creation
* [#4336](https://github.com/netbox-community/netbox/issues/4336) - Ensure interfaces without a subinterface ID are ordered before subinterface zero
* [#4361](https://github.com/netbox-community/netbox/issues/4361) - Fix Type of `connection_state` in Swagger schema
* [#4388](https://github.com/netbox-community/netbox/issues/4388) - Fix detection of connected endpoints when connecting rear ports
* [#4459](https://github.com/netbox-community/netbox/issues/4459) - Fix caching issue resulting in erroneous nested data for regions, rack groups, and tenant groups
* [#4489](https://github.com/netbox-community/netbox/issues/4489) - Fix display of parent/child role on device type view
* [#4496](https://github.com/netbox-community/netbox/issues/4496) - Fix exception when validating certain models via REST API
* [#4510](https://github.com/netbox-community/netbox/issues/4510) - Enforce address family for device primary IPv4/v6 addresses
---
## v2.8.0 (2020-04-13)
**NOTE:** Beginning with release 2.8.0, NetBox requires Python 3.6 or later.
### New Features (Beta)
This releases introduces two new features in beta status. While they are expected to be functional, their precise implementation is subject to change during the v2.8 release cycle. It is recommended to wait until NetBox v2.9 to deploy them in production.
#### Remote Authentication Support ([#2328](https://github.com/netbox-community/netbox/issues/2328))
Several new configuration parameters provide support for authenticating an incoming request based on the value of a specific HTTP header. This can be leveraged to employ remote authentication via an nginx or Apache plugin, directing NetBox to create and configure a local user account as needed. The configuration parameters are:
* `REMOTE_AUTH_ENABLED` - Enables remote authentication (disabled by default)
* `REMOTE_AUTH_HEADER` - The name of the HTTP header which conveys the username
* `REMOTE_AUTH_AUTO_CREATE_USER` - Enables the automatic creation of new users (disabled by default)
* `REMOTE_AUTH_DEFAULT_GROUPS` - A list of groups to assign newly created users
* `REMOTE_AUTH_DEFAULT_PERMISSIONS` - A list of permissions to assign newly created users
If further customization of remote authentication is desired (for instance, if you want to pass group/permission information via HTTP headers as well), NetBox allows you to inject a custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to retain full control over the authentication and configuration of remote users.
#### Plugins ([#3351](https://github.com/netbox-community/netbox/issues/3351))
This release introduces support for custom plugins, which can be used to extend NetBox's functionality beyond what the core product provides. For example, plugins can be used to:
* Add new Django models
* Provide new views with custom templates
* Inject custom template into object views
* Introduce new API endpoints
* Add custom request/response middleware
For NetBox plugins to be recognized, they must be installed and added by name to the `PLUGINS` configuration parameter. (Plugin support is disabled by default.) Plugins can be configured under the `PLUGINS_CONFIG` parameter. More information can be found the in the [plugins documentation](https://netbox.readthedocs.io/en/stable/plugins/).
### Enhancements
* [#1754](https://github.com/netbox-community/netbox/issues/1754) - Added support for nested rack groups
* [#3939](https://github.com/netbox-community/netbox/issues/3939) - Added support for nested tenant groups
* [#4078](https://github.com/netbox-community/netbox/issues/4078) - Standardized description fields across all models
* [#4195](https://github.com/netbox-community/netbox/issues/4195) - Enabled application logging (see [logging configuration](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#logging))
### Bug Fixes
* [#4474](https://github.com/netbox-community/netbox/issues/4474) - Fix population of device types when bulk editing devices
* [#4476](https://github.com/netbox-community/netbox/issues/4476) - Correct typo in slugs for Infiniband interface types
### API Changes
* The `_choices` API endpoints have been removed. Instead, use an `OPTIONS` request to a model's endpoint to view the available values for all fields. ([#3416](https://github.com/netbox-community/netbox/issues/3416))
* The `id__in` filter has been removed from all models ([#4313](https://github.com/netbox-community/netbox/issues/4313)). Use the format `?id=1&id=2` instead.
* dcim.Manufacturer: Added a `description` field
* dcim.Platform: Added a `description` field
* dcim.Rack: The `/api/dcim/racks/<pk>/units/` endpoint has been replaced with `/api/dcim/racks/<pk>/elevation/`.
* dcim.RackGroup: Added a `description` field
* dcim.Region: Added a `description` field
* extras.Tag: Renamed `comments` to `description`; truncated length to 200 characters; removed Markdown rendering
* ipam.RIR: Added a `description` field
* ipam.VLANGroup: Added a `description` field
* tenancy.TenantGroup: Added a `description` field
* virtualization.ClusterGroup: Added a `description` field
* virtualization.ClusterType: Added a `description` field
### Other Changes
* [#4081](https://github.com/netbox-community/netbox/issues/4081) - The `family` field has been removed from the Aggregate, Prefix, and IPAddress models. The field remains available in the API representations of these models, however the column has been removed from the database table.

View File

@ -7,11 +7,12 @@ python:
theme: theme:
name: readthedocs name: readthedocs
navigation_depth: 3 navigation_depth: 3
extra_css:
- extra.css
markdown_extensions: markdown_extensions:
- admonition: - admonition:
- markdown_include.include: - markdown_include.include:
headingOffset: 1 headingOffset: 1
nav: nav:
- Introduction: 'index.md' - Introduction: 'index.md'
- Installation: - Installation:
@ -53,6 +54,9 @@ nav:
- Reports: 'additional-features/reports.md' - Reports: 'additional-features/reports.md'
- Tags: 'additional-features/tags.md' - Tags: 'additional-features/tags.md'
- Webhooks: 'additional-features/webhooks.md' - Webhooks: 'additional-features/webhooks.md'
- Plugins:
- Using Plugins: 'plugins/index.md'
- Developing Plugins: 'plugins/development.md'
- Administration: - Administration:
- Replicating NetBox: 'administration/replicating-netbox.md' - Replicating NetBox: 'administration/replicating-netbox.md'
- NetBox Shell: 'administration/netbox-shell.md' - NetBox Shell: 'administration/netbox-shell.md'
@ -67,9 +71,11 @@ nav:
- Style Guide: 'development/style-guide.md' - Style Guide: 'development/style-guide.md'
- Utility Views: 'development/utility-views.md' - Utility Views: 'development/utility-views.md'
- Extending Models: 'development/extending-models.md' - Extending Models: 'development/extending-models.md'
- Application Registry: 'development/application-registry.md'
- User Preferences: 'development/user-preferences.md'
- Release Checklist: 'development/release-checklist.md' - Release Checklist: 'development/release-checklist.md'
- Squashing Migrations: 'development/squashing-migrations.md'
- Release Notes: - Release Notes:
- Version 2.8: 'release-notes/version-2.8.md'
- Version 2.7: 'release-notes/version-2.7.md' - Version 2.7: 'release-notes/version-2.7.md'
- Version 2.6: 'release-notes/version-2.6.md' - Version 2.6: 'release-notes/version-2.6.md'
- Version 2.5: 'release-notes/version-2.5.md' - Version 2.5: 'release-notes/version-2.5.md'

View File

@ -14,9 +14,6 @@ class CircuitsRootView(routers.APIRootView):
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.APIRootView = CircuitsRootView router.APIRootView = CircuitsRootView
# Field choices
router.register('_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice')
# Providers # Providers
router.register('providers', views.ProviderViewSet) router.register('providers', views.ProviderViewSet)

View File

@ -8,21 +8,10 @@ from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph from extras.models import Graph
from utilities.api import FieldChoicesViewSet, ModelViewSet from utilities.api import ModelViewSet
from . import serializers from . import serializers
#
# Field choices
#
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(serializers.CircuitSerializer, ['status']),
(serializers.CircuitTerminationSerializer, ['term_side']),
)
# #
# Providers # Providers
# #

View File

@ -5,7 +5,7 @@ from dcim.models import Region, Site
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet from tenancy.filters import TenancyFilterSet
from utilities.filters import ( from utilities.filters import (
BaseFilterSet, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
) )
from .choices import * from .choices import *
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -19,10 +19,6 @@ __all__ = (
class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -55,7 +51,7 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilte
class Meta: class Meta:
model = Provider model = Provider
fields = ['name', 'slug', 'asn', 'account'] fields = ['id', 'name', 'slug', 'asn', 'account']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -77,10 +73,6 @@ class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -137,7 +129,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, Cr
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['cid', 'install_date', 'commit_rate'] fields = ['id', 'cid', 'install_date', 'commit_rate']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -1,16 +1,16 @@
from django import forms from django import forms
from taggit.forms import TagField
from dcim.models import Region, Site from dcim.models import Region, Site
from extras.forms import ( from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
TagField,
) )
from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2, CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField,
StaticSelect2Multiple, TagFilterField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
) )
from .choices import CircuitStatusChoices from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -55,12 +55,6 @@ class ProviderCSVForm(CustomFieldModelCSVForm):
class Meta: class Meta:
model = Provider model = Provider
fields = Provider.csv_headers fields = Provider.csv_headers
help_texts = {
'name': 'Provider name',
'asn': '32-bit autonomous system number',
'portal_url': 'Portal URL',
'comments': 'Free-form comments',
}
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
@ -113,7 +107,6 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug", value_field="slug",
filter_for={ filter_for={
'site': 'region' 'site': 'region'
@ -125,7 +118,6 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
) )
) )
@ -150,7 +142,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
] ]
class CircuitTypeCSVForm(forms.ModelForm): class CircuitTypeCSVForm(CSVModelForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -167,16 +159,10 @@ class CircuitTypeCSVForm(forms.ModelForm):
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all()
widget=APISelect(
api_url="/api/circuits/providers/"
)
) )
type = DynamicModelChoiceField( type = DynamicModelChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all()
widget=APISelect(
api_url="/api/circuits/circuit-types/"
)
) )
comments = CommentField() comments = CommentField()
tags = TagField( tags = TagField(
@ -200,35 +186,26 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class CircuitCSVForm(CustomFieldModelCSVForm): class CircuitCSVForm(CustomFieldModelCSVForm):
provider = forms.ModelChoiceField( provider = CSVModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='name', to_field_name='name',
help_text='Name of parent provider', help_text='Assigned provider'
error_messages={
'invalid_choice': 'Provider not found.'
}
) )
type = forms.ModelChoiceField( type = CSVModelChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
to_field_name='name', to_field_name='name',
help_text='Type of circuit', help_text='Type of circuit'
error_messages={
'invalid_choice': 'Invalid circuit type.'
}
) )
status = CSVChoiceField( status = CSVChoiceField(
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
required=False, required=False,
help_text='Operational status' help_text='Operational status'
) )
tenant = forms.ModelChoiceField( tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text='Name of assigned tenant', help_text='Assigned tenant'
error_messages={
'invalid_choice': 'Tenant not found.'
}
) )
class Meta: class Meta:
@ -245,17 +222,11 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
) )
type = DynamicModelChoiceField( type = DynamicModelChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
required=False, required=False
widget=APISelect(
api_url="/api/circuits/circuit-types/"
)
) )
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
required=False, required=False
widget=APISelect(
api_url="/api/circuits/providers/"
)
) )
status = forms.ChoiceField( status = forms.ChoiceField(
choices=add_blank_choice(CircuitStatusChoices), choices=add_blank_choice(CircuitStatusChoices),
@ -265,10 +236,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
) )
commit_rate = forms.IntegerField( commit_rate = forms.IntegerField(
required=False, required=False,
@ -303,7 +271,6 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/circuits/circuit-types/",
value_field="slug", value_field="slug",
) )
) )
@ -312,7 +279,6 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/circuits/providers/",
value_field="slug", value_field="slug",
) )
) )
@ -326,7 +292,6 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug", value_field="slug",
filter_for={ filter_for={
'site': 'region' 'site': 'region'
@ -338,7 +303,6 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
) )
) )
@ -355,6 +319,9 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
# #
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
site = DynamicModelChoiceField(
queryset=Site.objects.all()
)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
@ -368,7 +335,4 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
} }
widgets = { widgets = {
'term_side': forms.HiddenInput(), 'term_side': forms.HiddenInput(),
'site': APISelect(
api_url="/api/dcim/sites/"
)
} }

View File

@ -1,134 +0,0 @@
import django.db.models.deletion
from django.db import migrations, models
import dcim.fields
def circuits_to_terms(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
for c in Circuit.objects.all():
CircuitTermination(
circuit=c,
term_side=b'A',
site=c.site,
interface=c.interface,
port_speed=c.port_speed,
upstream_speed=c.upstream_speed,
xconnect_id=c.xconnect_id,
pp_info=c.pp_info,
).save()
class Migration(migrations.Migration):
replaces = [('circuits', '0001_initial'), ('circuits', '0002_auto_20160622_1821'), ('circuits', '0003_provider_32bit_asn_support'), ('circuits', '0004_circuit_add_tenant'), ('circuits', '0005_circuit_add_upstream_speed'), ('circuits', '0006_terminations')]
dependencies = [
('tenancy', '0001_initial'),
('dcim', '0001_initial'),
('dcim', '0022_color_names_to_rgb'),
]
operations = [
migrations.CreateModel(
name='CircuitType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
('asn', dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN')),
('account', models.CharField(blank=True, max_length=30, verbose_name=b'Account number')),
('portal_url', models.URLField(blank=True, verbose_name=b'Portal')),
('noc_contact', models.TextField(blank=True, verbose_name=b'NOC contact')),
('admin_contact', models.TextField(blank=True, verbose_name=b'Admin contact')),
('comments', models.TextField(blank=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Circuit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('cid', models.CharField(max_length=50, verbose_name=b'Circuit ID')),
('install_date', models.DateField(blank=True, null=True, verbose_name=b'Date installed')),
('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')),
('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'Commit rate (Kbps)')),
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')),
('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')),
('comments', models.TextField(blank=True)),
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit', to='dcim.Interface')),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.Provider')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='dcim.Site')),
('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.CircuitType')),
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant')),
('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')),
],
options={
'ordering': ['provider', 'cid'],
'unique_together': {('provider', 'cid')},
},
),
migrations.CreateModel(
name='CircuitTermination',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('term_side', models.CharField(choices=[(b'A', b'A'), (b'Z', b'Z')], max_length=1, verbose_name='Termination')),
('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')),
('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')),
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')),
('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')),
('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.Circuit')),
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit_termination', to='dcim.Interface')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.Site')),
],
options={
'ordering': ['circuit', 'term_side'],
'unique_together': {('circuit', 'term_side')},
},
),
migrations.RunPython(
code=circuits_to_terms,
),
migrations.RemoveField(
model_name='circuit',
name='interface',
),
migrations.RemoveField(
model_name='circuit',
name='port_speed',
),
migrations.RemoveField(
model_name='circuit',
name='pp_info',
),
migrations.RemoveField(
model_name='circuit',
name='site',
),
migrations.RemoveField(
model_name='circuit',
name='upstream_speed',
),
migrations.RemoveField(
model_name='circuit',
name='xconnect_id',
),
]

View File

@ -1,254 +0,0 @@
import sys
import django.db.models.deletion
import taggit.managers
from django.db import migrations, models
import dcim.fields
CONNECTION_STATUS_CONNECTED = True
CIRCUIT_STATUS_CHOICES = (
(0, 'deprovisioning'),
(1, 'active'),
(2, 'planned'),
(3, 'provisioning'),
(4, 'offline'),
(5, 'decommissioned')
)
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))
def circuit_status_to_slug(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit')
for id, slug in CIRCUIT_STATUS_CHOICES:
Circuit.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
replaces = [('circuits', '0007_circuit_add_description'), ('circuits', '0008_circuittermination_interface_protect_on_delete'), ('circuits', '0009_unicode_literals'), ('circuits', '0010_circuit_status'), ('circuits', '0011_tags'), ('circuits', '0012_change_logging'), ('circuits', '0013_cables'), ('circuits', '0014_circuittermination_description'), ('circuits', '0015_custom_tag_models'), ('circuits', '0016_3569_circuit_fields'), ('circuits', '0017_circuittype_description')]
dependencies = [
('circuits', '0006_terminations'),
('extras', '0019_tag_taggeditem'),
('taggit', '0002_auto_20150616_2121'),
('dcim', '0066_cables'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AlterField(
model_name='circuittermination',
name='interface',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface'),
),
migrations.AlterField(
model_name='circuit',
name='cid',
field=models.CharField(max_length=50, verbose_name='Circuit ID'),
),
migrations.AlterField(
model_name='circuit',
name='commit_rate',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'),
),
migrations.AlterField(
model_name='circuit',
name='install_date',
field=models.DateField(blank=True, null=True, verbose_name='Date installed'),
),
migrations.AlterField(
model_name='circuittermination',
name='port_speed',
field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'),
),
migrations.AlterField(
model_name='circuittermination',
name='pp_info',
field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'),
),
migrations.AlterField(
model_name='circuittermination',
name='term_side',
field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'),
),
migrations.AlterField(
model_name='circuittermination',
name='upstream_speed',
field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'),
),
migrations.AlterField(
model_name='circuittermination',
name='xconnect_id',
field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'),
),
migrations.AlterField(
model_name='provider',
name='account',
field=models.CharField(blank=True, max_length=30, verbose_name='Account number'),
),
migrations.AlterField(
model_name='provider',
name='admin_contact',
field=models.TextField(blank=True, verbose_name='Admin contact'),
),
migrations.AlterField(
model_name='provider',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
),
migrations.AlterField(
model_name='provider',
name='noc_contact',
field=models.TextField(blank=True, verbose_name='NOC contact'),
),
migrations.AlterField(
model_name='provider',
name='portal_url',
field=models.URLField(blank=True, verbose_name='Portal'),
),
migrations.AddField(
model_name='circuit',
name='status',
field=models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1),
),
migrations.AddField(
model_name='circuit',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='provider',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='circuittype',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='circuittype',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='circuit',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='circuit',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='provider',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='provider',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
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(),
),
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'),
),
migrations.RunPython(
code=circuit_terminations_to_cables,
),
migrations.RemoveField(
model_name='circuittermination',
name='interface',
),
migrations.AddField(
model_name='circuittermination',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AlterField(
model_name='circuit',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='provider',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='circuit',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=circuit_status_to_slug,
),
migrations.AddField(
model_name='circuittype',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.0.3 on 2020-03-13 20:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0017_circuittype_description'),
]
operations = [
migrations.AlterField(
model_name='circuit',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='circuittermination',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='circuittype',
name='description',
field=models.CharField(blank=True, max_length=200),
),
]

View File

@ -7,6 +7,7 @@ from dcim.constants import CONNECTION_STATUS_CHOICES
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.models import CableTermination from dcim.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange, TaggedItem from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_features
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object from utilities.utils import serialize_object
from .choices import * from .choices import *
@ -21,6 +22,7 @@ __all__ = (
) )
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
class Provider(ChangeLoggedModel, CustomFieldModel): class Provider(ChangeLoggedModel, CustomFieldModel):
""" """
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
@ -36,7 +38,8 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
asn = ASNField( asn = ASNField(
blank=True, blank=True,
null=True, null=True,
verbose_name='ASN' verbose_name='ASN',
help_text='32-bit autonomous system number'
) )
account = models.CharField( account = models.CharField(
max_length=30, max_length=30,
@ -45,7 +48,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
) )
portal_url = models.URLField( portal_url = models.URLField(
blank=True, blank=True,
verbose_name='Portal' verbose_name='Portal URL'
) )
noc_contact = models.TextField( noc_contact = models.TextField(
blank=True, blank=True,
@ -108,7 +111,7 @@ class CircuitType(ChangeLoggedModel):
unique=True unique=True
) )
description = models.CharField( description = models.CharField(
max_length=100, max_length=200,
blank=True, blank=True,
) )
@ -131,6 +134,7 @@ class CircuitType(ChangeLoggedModel):
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Circuit(ChangeLoggedModel, CustomFieldModel): class Circuit(ChangeLoggedModel, CustomFieldModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
@ -173,7 +177,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
null=True, null=True,
verbose_name='Commit rate (Kbps)') verbose_name='Commit rate (Kbps)')
description = models.CharField( description = models.CharField(
max_length=100, max_length=200,
blank=True blank=True
) )
comments = models.TextField( comments = models.TextField(
@ -292,7 +296,7 @@ class CircuitTermination(CableTermination):
verbose_name='Patch panel/port(s)' verbose_name='Patch panel/port(s)'
) )
description = models.CharField( description = models.CharField(
max_length=100, max_length=200,
blank=True blank=True
) )

View File

@ -2,7 +2,7 @@ import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, TagColumn, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_ACTIONS = """ CIRCUITTYPE_ACTIONS = """
@ -27,18 +27,20 @@ STATUS_LABEL = """
class ProviderTable(BaseTable): class ProviderTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
circuit_count = tables.Column(
accessor=Accessor('count_circuits'),
verbose_name='Circuits'
)
tags = TagColumn(
url_name='circuits:provider_list'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Provider model = Provider
fields = ('pk', 'name', 'asn', 'account',) fields = (
'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'tags',
)
class ProviderDetailTable(ProviderTable): default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(ProviderTable.Meta):
model = Provider
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
# #
@ -48,7 +50,9 @@ class ProviderDetailTable(ProviderTable):
class CircuitTypeTable(BaseTable): class CircuitTypeTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
circuit_count = tables.Column(verbose_name='Circuits') circuit_count = tables.Column(
verbose_name='Circuits'
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=CIRCUITTYPE_ACTIONS, template_code=CIRCUITTYPE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -58,6 +62,7 @@ class CircuitTypeTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CircuitType model = CircuitType
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
# #
@ -66,17 +71,33 @@ class CircuitTypeTable(BaseTable):
class CircuitTable(BaseTable): class CircuitTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
cid = tables.LinkColumn(verbose_name='ID') cid = tables.LinkColumn(
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) verbose_name='ID'
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') )
tenant = tables.TemplateColumn(template_code=COL_TENANT) provider = tables.LinkColumn(
viewname='circuits:provider',
args=[Accessor('provider.slug')]
)
status = tables.TemplateColumn(
template_code=STATUS_LABEL
)
tenant = tables.TemplateColumn(
template_code=COL_TENANT
)
a_side = tables.Column( a_side = tables.Column(
verbose_name='A Side' verbose_name='A Side'
) )
z_side = tables.Column( z_side = tables.Column(
verbose_name='Z Side' verbose_name='Z Side'
) )
tags = TagColumn(
url_name='circuits:circuit_list'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Circuit model = Circuit
fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description') fields = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate',
'description', 'tags',
)
default_columns = ('pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'description')

View File

@ -6,7 +6,7 @@ from circuits.choices import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Site from dcim.models import Site
from extras.models import Graph from extras.models import Graph
from utilities.testing import APITestCase, choices_to_dict from utilities.testing import APITestCase
class AppTest(APITestCase): class AppTest(APITestCase):
@ -18,19 +18,6 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_choices(self):
url = reverse('circuits-api:field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
# Circuit
self.assertEqual(choices_to_dict(response.data.get('circuit:status')), CircuitStatusChoices.as_dict())
# CircuitTermination
self.assertEqual(choices_to_dict(response.data.get('circuit-termination:term_side')), CircuitTerminationSideChoices.as_dict())
class ProviderTest(APITestCase): class ProviderTest(APITestCase):

View File

@ -54,6 +54,10 @@ class ProviderTestCase(TestCase):
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A', port_speed=1000), CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A', port_speed=1000),
)) ))
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
params = {'name': ['Provider 1', 'Provider 2']} params = {'name': ['Provider 1', 'Provider 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -70,11 +74,6 @@ class ProviderTestCase(TestCase):
params = {'account': ['1234', '2345']} params = {'account': ['1234', '2345']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_id__in(self):
id_list = self.queryset.values_list('id', flat=True)[:3]
params = {'id__in': ','.join([str(id) for id in id_list])}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_site(self): def test_site(self):
sites = Site.objects.all()[:2] sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]} params = {'site_id': [sites[0].pk, sites[1].pk]}
@ -144,7 +143,8 @@ class CircuitTestCase(TestCase):
TenantGroup(name='Tenant group 2', slug='tenant-group-2'), TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'), TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
) )
TenantGroup.objects.bulk_create(tenant_groups) for tenantgroup in tenant_groups:
tenantgroup.save()
tenants = ( tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
@ -182,6 +182,10 @@ class CircuitTestCase(TestCase):
)) ))
CircuitTermination.objects.bulk_create(circuit_terminations) CircuitTermination.objects.bulk_create(circuit_terminations)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cid(self): def test_cid(self):
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']} params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -194,11 +198,6 @@ class CircuitTestCase(TestCase):
params = {'commit_rate': ['1000', '2000']} params = {'commit_rate': ['1000', '2000']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_id__in(self):
id_list = self.queryset.values_list('id', flat=True)[:3]
params = {'id__in': ','.join([str(id) for id in id_list])}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_provider(self): def test_provider(self):
provider = Provider.objects.first() provider = Provider.objects.first()
params = {'provider_id': [provider.pk]} params = {'provider_id': [provider.pk]}

View File

@ -28,7 +28,7 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits')) queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filterset = filters.ProviderFilterSet filterset = filters.ProviderFilterSet
filterset_form = forms.ProviderFilterForm filterset_form = forms.ProviderFilterForm
table = tables.ProviderDetailTable table = tables.ProviderTable
class ProviderView(PermissionRequiredMixin, View): class ProviderView(PermissionRequiredMixin, View):
@ -87,7 +87,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_provider' permission_required = 'circuits.change_provider'
queryset = Provider.objects.all() queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filterset = filters.ProviderFilterSet filterset = filters.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
form = forms.ProviderBulkEditForm form = forms.ProviderBulkEditForm
@ -96,7 +96,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider' permission_required = 'circuits.delete_provider'
queryset = Provider.objects.all() queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filterset = filters.ProviderFilterSet filterset = filters.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
default_return_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'

View File

@ -64,7 +64,7 @@ class RegionSerializer(CustomFieldModelSerializer):
class Meta: class Meta:
model = Region model = Region
fields = ['id', 'name', 'slug', 'parent', 'site_count', 'custom_fields'] fields = ['id', 'name', 'slug', 'parent', 'description', 'site_count', 'custom_fields']
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer): class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
@ -96,11 +96,12 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
class RackGroupSerializer(ValidatedModelSerializer): class RackGroupSerializer(ValidatedModelSerializer):
site = NestedSiteSerializer() site = NestedSiteSerializer()
parent = NestedRackGroupSerializer(required=False, allow_null=True)
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = RackGroup model = RackGroup
fields = ['id', 'name', 'slug', 'site', 'rack_count'] fields = ['id', 'name', 'slug', 'site', 'parent', 'description', 'rack_count']
class RackRoleSerializer(ValidatedModelSerializer): class RackRoleSerializer(ValidatedModelSerializer):
@ -142,8 +143,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
# Validate uniqueness of (group, facility_id) since we omitted the automatically-created validator from Meta. # Validate uniqueness of (group, facility_id) since we omitted the automatically-created validator from Meta.
if data.get('facility_id', None): if data.get('facility_id', None):
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'facility_id')) validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'facility_id'))
validator.set_context(self) validator(data, self)
validator(data)
# Enforce model validation # Enforce model validation
super().validate(data) super().validate(data)
@ -218,7 +218,9 @@ class ManufacturerSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ['id', 'name', 'slug', 'devicetype_count', 'inventoryitem_count', 'platform_count'] fields = [
'id', 'name', 'slug', 'description', 'devicetype_count', 'inventoryitem_count', 'platform_count',
]
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
@ -355,7 +357,7 @@ class PlatformSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Platform model = Platform
fields = [ fields = [
'id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'device_count', 'id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'device_count',
'virtualmachine_count', 'virtualmachine_count',
] ]
@ -392,8 +394,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
# Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta. # Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta.
if data.get('rack') and data.get('position') and data.get('face'): if data.get('rack') and data.get('position') and data.get('face'):
validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face')) validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face'))
validator.set_context(self) validator(data, self)
validator(data)
# Enforce model validation # Enforce model validation
super().validate(data) super().validate(data)
@ -530,6 +531,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
) )
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
count_ipaddresses = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = Interface model = Interface

View File

@ -14,9 +14,6 @@ class DCIMRootView(routers.APIRootView):
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.APIRootView = DCIMRootView router.APIRootView = DCIMRootView
# Field choices
router.register('_choices', views.DCIMFieldChoicesViewSet, basename='field-choice')
# Sites # Sites
router.register('regions', views.RegionViewSet) router.register('regions', views.RegionViewSet)
router.register('sites', views.SiteViewSet) router.register('sites', views.SiteViewSet)

View File

@ -26,7 +26,7 @@ from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph from extras.models import Graph
from ipam.models import Prefix, VLAN from ipam.models import Prefix, VLAN
from utilities.api import ( from utilities.api import (
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable, get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
) )
from utilities.utils import get_subquery from utilities.utils import get_subquery
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
@ -34,35 +34,6 @@ from . import serializers
from .exceptions import MissingFilterException from .exceptions import MissingFilterException
#
# Field choices
#
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(serializers.CableSerializer, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
(serializers.ConsolePortSerializer, ['type', 'connection_status']),
(serializers.ConsolePortTemplateSerializer, ['type']),
(serializers.ConsoleServerPortSerializer, ['type']),
(serializers.ConsoleServerPortTemplateSerializer, ['type']),
(serializers.DeviceSerializer, ['face', 'status']),
(serializers.DeviceTypeSerializer, ['subdevice_role']),
(serializers.FrontPortSerializer, ['type']),
(serializers.FrontPortTemplateSerializer, ['type']),
(serializers.InterfaceSerializer, ['type', 'mode']),
(serializers.InterfaceTemplateSerializer, ['type']),
(serializers.PowerFeedSerializer, ['phase', 'status', 'supply', 'type']),
(serializers.PowerOutletSerializer, ['type', 'feed_leg']),
(serializers.PowerOutletTemplateSerializer, ['type', 'feed_leg']),
(serializers.PowerPortSerializer, ['type', 'connection_status']),
(serializers.PowerPortTemplateSerializer, ['type']),
(serializers.RackSerializer, ['outer_unit', 'status', 'type', 'width']),
(serializers.RearPortSerializer, ['type']),
(serializers.RearPortTemplateSerializer, ['type']),
(serializers.SiteSerializer, ['status']),
)
# Mixins # Mixins
class CableTraceMixin(object): class CableTraceMixin(object):
@ -77,7 +48,7 @@ class CableTraceMixin(object):
# Initialize the path array # Initialize the path array
path = [] path = []
for near_end, cable, far_end in obj.trace(follow_circuits=True): for near_end, cable, far_end in obj.trace()[0]:
# Serialize each object # Serialize each object
serializer_a = get_serializer_for_model(near_end, prefix='Nested') serializer_a = get_serializer_for_model(near_end, prefix='Nested')
@ -176,33 +147,6 @@ class RackViewSet(CustomFieldModelViewSet):
serializer_class = serializers.RackSerializer serializer_class = serializers.RackSerializer
filterset_class = filters.RackFilterSet filterset_class = filters.RackFilterSet
@swagger_auto_schema(deprecated=True)
@action(detail=True)
def units(self, request, pk=None):
"""
List rack units (by rack)
"""
# TODO: Remove this action detail route in v2.8
rack = get_object_or_404(Rack, pk=pk)
face = request.GET.get('face', 'front')
exclude_pk = request.GET.get('exclude', None)
if exclude_pk is not None:
try:
exclude_pk = int(exclude_pk)
except ValueError:
exclude_pk = None
elevation = rack.get_rack_units(face, exclude_pk)
# Enable filtering rack units by ID
q = request.GET.get('q', None)
if q:
elevation = [u for u in elevation if q in str(u['id'])]
page = self.paginate_queryset(elevation)
if page is not None:
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(rack_units.data)
@swagger_auto_schema( @swagger_auto_schema(
responses={200: serializers.RackUnitSerializer(many=True)}, responses={200: serializers.RackUnitSerializer(many=True)},
query_serializer=serializers.RackElevationDetailFilterSerializer query_serializer=serializers.RackElevationDetailFilterSerializer
@ -225,7 +169,8 @@ class RackViewSet(CustomFieldModelViewSet):
unit_width=data['unit_width'], unit_width=data['unit_width'],
unit_height=data['unit_height'], unit_height=data['unit_height'],
legend_width=data['legend_width'], legend_width=data['legend_width'],
include_images=data['include_images'] include_images=data['include_images'],
base_url=request.build_absolute_uri('/')
) )
return HttpResponse(drawing.tostring(), content_type='image/svg+xml') return HttpResponse(drawing.tostring(), content_type='image/svg+xml')

View File

@ -63,11 +63,13 @@ class RackWidthChoices(ChoiceSet):
WIDTH_10IN = 10 WIDTH_10IN = 10
WIDTH_19IN = 19 WIDTH_19IN = 19
WIDTH_21IN = 21
WIDTH_23IN = 23 WIDTH_23IN = 23
CHOICES = ( CHOICES = (
(WIDTH_10IN, '10 inches'), (WIDTH_10IN, '10 inches'),
(WIDTH_19IN, '19 inches'), (WIDTH_19IN, '19 inches'),
(WIDTH_21IN, '21 inches'),
(WIDTH_23IN, '23 inches'), (WIDTH_23IN, '23 inches'),
) )
@ -280,6 +282,10 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_NEMA_L620P = 'nema-l6-20p' TYPE_NEMA_L620P = 'nema-l6-20p'
TYPE_NEMA_L630P = 'nema-l6-30p' TYPE_NEMA_L630P = 'nema-l6-30p'
TYPE_NEMA_L650P = 'nema-l6-50p' TYPE_NEMA_L650P = 'nema-l6-50p'
TYPE_NEMA_L1420P = 'nema-l14-20p'
TYPE_NEMA_L1430P = 'nema-l14-30p'
TYPE_NEMA_L2120P = 'nema-l21-20p'
TYPE_NEMA_L2130P = 'nema-l21-30p'
# California style # California style
TYPE_CS6361C = 'cs6361c' TYPE_CS6361C = 'cs6361c'
TYPE_CS6365C = 'cs6365c' TYPE_CS6365C = 'cs6365c'
@ -341,6 +347,10 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEMA_L620P, 'NEMA L6-20P'), (TYPE_NEMA_L620P, 'NEMA L6-20P'),
(TYPE_NEMA_L630P, 'NEMA L6-30P'), (TYPE_NEMA_L630P, 'NEMA L6-30P'),
(TYPE_NEMA_L650P, 'NEMA L6-50P'), (TYPE_NEMA_L650P, 'NEMA L6-50P'),
(TYPE_NEMA_L1420P, 'NEMA L14-20P'),
(TYPE_NEMA_L1430P, 'NEMA L14-30P'),
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
)), )),
('California Style', ( ('California Style', (
(TYPE_CS6361C, 'CS6361C'), (TYPE_CS6361C, 'CS6361C'),
@ -409,6 +419,10 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_NEMA_L620R = 'nema-l6-20r' TYPE_NEMA_L620R = 'nema-l6-20r'
TYPE_NEMA_L630R = 'nema-l6-30r' TYPE_NEMA_L630R = 'nema-l6-30r'
TYPE_NEMA_L650R = 'nema-l6-50r' TYPE_NEMA_L650R = 'nema-l6-50r'
TYPE_NEMA_L1420R = 'nema-l14-20r'
TYPE_NEMA_L1430R = 'nema-l14-30r'
TYPE_NEMA_L2120R = 'nema-l21-20r'
TYPE_NEMA_L2130R = 'nema-l21-30r'
# California style # California style
TYPE_CS6360C = 'CS6360C' TYPE_CS6360C = 'CS6360C'
TYPE_CS6364C = 'CS6364C' TYPE_CS6364C = 'CS6364C'
@ -428,6 +442,8 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_ITA_M = 'ita-m' TYPE_ITA_M = 'ita-m'
TYPE_ITA_N = 'ita-n' TYPE_ITA_N = 'ita-n'
TYPE_ITA_O = 'ita-o' TYPE_ITA_O = 'ita-o'
# Proprietary
TYPE_HDOT_CX = 'hdot-cx'
CHOICES = ( CHOICES = (
('IEC 60320', ( ('IEC 60320', (
@ -469,6 +485,10 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_NEMA_L620R, 'NEMA L6-20R'), (TYPE_NEMA_L620R, 'NEMA L6-20R'),
(TYPE_NEMA_L630R, 'NEMA L6-30R'), (TYPE_NEMA_L630R, 'NEMA L6-30R'),
(TYPE_NEMA_L650R, 'NEMA L6-50R'), (TYPE_NEMA_L650R, 'NEMA L6-50R'),
(TYPE_NEMA_L1420R, 'NEMA L14-20R'),
(TYPE_NEMA_L1430R, 'NEMA L14-30R'),
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
)), )),
('California Style', ( ('California Style', (
(TYPE_CS6360C, 'CS6360C'), (TYPE_CS6360C, 'CS6360C'),
@ -491,6 +511,9 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_ITA_N, 'ITA Type N'), (TYPE_ITA_N, 'ITA Type N'),
(TYPE_ITA_O, 'ITA Type O'), (TYPE_ITA_O, 'ITA Type O'),
)), )),
('Proprietary', (
(TYPE_HDOT_CX, 'HDOT Cx'),
)),
) )
@ -581,15 +604,15 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_128GFC_QSFP28 = '128gfc-sfp28' TYPE_128GFC_QSFP28 = '128gfc-sfp28'
# InfiniBand # InfiniBand
TYPE_INFINIBAND_SDR = 'inifiband-sdr' TYPE_INFINIBAND_SDR = 'infiniband-sdr'
TYPE_INFINIBAND_DDR = 'inifiband-ddr' TYPE_INFINIBAND_DDR = 'infiniband-ddr'
TYPE_INFINIBAND_QDR = 'inifiband-qdr' TYPE_INFINIBAND_QDR = 'infiniband-qdr'
TYPE_INFINIBAND_FDR10 = 'inifiband-fdr10' TYPE_INFINIBAND_FDR10 = 'infiniband-fdr10'
TYPE_INFINIBAND_FDR = 'inifiband-fdr' TYPE_INFINIBAND_FDR = 'infiniband-fdr'
TYPE_INFINIBAND_EDR = 'inifiband-edr' TYPE_INFINIBAND_EDR = 'infiniband-edr'
TYPE_INFINIBAND_HDR = 'inifiband-hdr' TYPE_INFINIBAND_HDR = 'infiniband-hdr'
TYPE_INFINIBAND_NDR = 'inifiband-ndr' TYPE_INFINIBAND_NDR = 'infiniband-ndr'
TYPE_INFINIBAND_XDR = 'inifiband-xdr' TYPE_INFINIBAND_XDR = 'infiniband-xdr'
# Serial # Serial
TYPE_T1 = 't1' TYPE_T1 = 't1'

View File

@ -92,5 +92,5 @@ COMPATIBLE_TERMINATION_TYPES = {
'interface': ['interface', 'circuittermination', 'frontport', 'rearport'], 'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'], 'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'], 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
'circuittermination': ['interface', 'frontport', 'rearport'], 'circuittermination': ['interface', 'frontport', 'rearport', 'circuittermination'],
} }

View File

@ -15,10 +15,15 @@ class RackElevationSVG:
:param rack: A NetBox Rack instance :param rack: A NetBox Rack instance
:param include_images: If true, the SVG document will embed front/rear device face images, where available :param include_images: If true, the SVG document will embed front/rear device face images, where available
:param base_url: Base URL for links within the SVG document. If none, links will be relative.
""" """
def __init__(self, rack, include_images=True): def __init__(self, rack, include_images=True, base_url=None):
self.rack = rack self.rack = rack
self.include_images = include_images self.include_images = include_images
if base_url is not None:
self.base_url = base_url.rstrip('/')
else:
self.base_url = ''
def _get_device_description(self, device): def _get_device_description(self, device):
return '{} ({}) — {} ({}U) {} {}'.format( return '{} ({}) — {} ({}U) {} {}'.format(
@ -69,7 +74,7 @@ class RackElevationSVG:
color = device.device_role.color color = device.device_role.color
link = drawing.add( link = drawing.add(
drawing.a( drawing.a(
href=reverse('dcim:device', kwargs={'pk': device.pk}), href='{}{}'.format(self.base_url, reverse('dcim:device', kwargs={'pk': device.pk})),
target='_top', target='_top',
fill='black' fill='black'
) )
@ -81,7 +86,7 @@ class RackElevationSVG:
# Embed front device type image if one exists # Embed front device type image if one exists
if self.include_images and device.device_type.front_image: if self.include_images and device.device_type.front_image:
url = device.device_type.front_image.url url = '{}{}'.format(self.base_url, device.device_type.front_image.url)
image = drawing.image(href=url, insert=start, size=end, class_='device-image') image = drawing.image(href=url, insert=start, size=end, class_='device-image')
image.fit(scale='slice') image.fit(scale='slice')
link.add(image) link.add(image)

View File

@ -3,3 +3,12 @@ class LoopDetected(Exception):
A loop has been detected while tracing a cable path. A loop has been detected while tracing a cable path.
""" """
pass pass
class CableTraceSplit(Exception):
"""
A cable trace cannot be completed because a RearPort maps to multiple FrontPorts and
we don't know which one to follow.
"""
def __init__(self, termination, *args, **kwargs):
self.termination = termination

View File

@ -4,10 +4,10 @@ from django.contrib.auth.models import User
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet from tenancy.filters import TenancyFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES from utilities.choices import ColorChoices
from utilities.filters import ( from utilities.filters import (
BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .choices import * from .choices import *
@ -74,14 +74,10 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CustomFieldFilterS
class Meta: class Meta:
model = Region model = Region
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug', 'description']
class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -157,10 +153,20 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=RackGroup.objects.all(),
label='Rack group (ID)',
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=RackGroup.objects.all(),
to_field_name='slug',
label='Rack group (slug)',
)
class Meta: class Meta:
model = RackGroup model = RackGroup
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug', 'description']
class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
@ -171,10 +177,6 @@ class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -202,15 +204,18 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Creat
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = django_filters.ModelMultipleChoiceFilter( group_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
label='Group (ID)', field_name='group',
lookup_expr='in',
label='Rack group (ID)',
) )
group = django_filters.ModelMultipleChoiceFilter( group = TreeNodeMultipleChoiceFilter(
field_name='group__slug',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
field_name='group',
lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Group', label='Rack group (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=RackStatusChoices, choices=RackStatusChoices,
@ -251,10 +256,6 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Creat
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet): class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -274,16 +275,18 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = django_filters.ModelMultipleChoiceFilter( group_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(),
field_name='rack__group', field_name='rack__group',
queryset=RackGroup.objects.all(), lookup_expr='in',
label='Group (ID)', label='Rack group (ID)',
) )
group = django_filters.ModelMultipleChoiceFilter( group = TreeNodeMultipleChoiceFilter(
field_name='rack__group__slug',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
field_name='rack__group',
lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Group', label='Rack group (slug)',
) )
user_id = django_filters.ModelMultipleChoiceFilter( user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(), queryset=User.objects.all(),
@ -298,7 +301,7 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['created'] fields = ['id', 'created']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -315,14 +318,10 @@ class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug', 'description']
class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -370,7 +369,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFil
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ fields = [
'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
] ]
def search(self, queryset, name, value): def search(self, queryset, name, value):
@ -494,7 +493,7 @@ class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class Meta: class Meta:
model = Platform model = Platform
fields = ['id', 'name', 'slug', 'napalm_driver'] fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
class DeviceFilterSet( class DeviceFilterSet(
@ -504,10 +503,6 @@ class DeviceFilterSet(
CustomFieldFilterSet, CustomFieldFilterSet,
CreatedUpdatedFilterSet CreatedUpdatedFilterSet
): ):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -571,9 +566,10 @@ class DeviceFilterSet(
to_field_name='slug', to_field_name='slug',
label='Site name (slug)', label='Site name (slug)',
) )
rack_group_id = django_filters.ModelMultipleChoiceFilter( rack_group_id = TreeNodeMultipleChoiceFilter(
field_name='rack__group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
field_name='rack__group',
lookup_expr='in',
label='Rack group (ID)', label='Rack group (ID)',
) )
rack_id = django_filters.ModelMultipleChoiceFilter( rack_id = django_filters.ModelMultipleChoiceFilter(
@ -1088,7 +1084,7 @@ class CableFilterSet(BaseFilterSet):
choices=CableStatusChoices choices=CableStatusChoices
) )
color = django_filters.MultipleChoiceFilter( color = django_filters.MultipleChoiceFilter(
choices=COLOR_CHOICES choices=ColorChoices
) )
device_id = MultiValueNumberFilter( device_id = MultiValueNumberFilter(
method='filter_device' method='filter_device'
@ -1236,10 +1232,6 @@ class InterfaceConnectionFilterSet(BaseFilterSet):
class PowerPanelFilterSet(BaseFilterSet): class PowerPanelFilterSet(BaseFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -1267,15 +1259,16 @@ class PowerPanelFilterSet(BaseFilterSet):
to_field_name='slug', to_field_name='slug',
label='Site name (slug)', label='Site name (slug)',
) )
rack_group_id = django_filters.ModelMultipleChoiceFilter( rack_group_id = TreeNodeMultipleChoiceFilter(
field_name='rack_group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
field_name='rack_group',
lookup_expr='in',
label='Rack group (ID)', label='Rack group (ID)',
) )
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = ['name'] fields = ['id', 'name']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1287,10 +1280,6 @@ class PowerPanelFilterSet(BaseFilterSet):
class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -1332,7 +1321,7 @@ class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
class Meta: class Meta:
model = PowerFeed model = PowerFeed
fields = ['name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'] fields = ['id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +0,0 @@
import django.db.models.deletion
from django.db import migrations, models
import dcim.fields
def copy_primary_ip(apps, schema_editor):
Device = apps.get_model('dcim', 'Device')
for d in Device.objects.select_related('primary_ip'):
if not d.primary_ip:
continue
if d.primary_ip.family == 4:
d.primary_ip4 = d.primary_ip
elif d.primary_ip.family == 6:
d.primary_ip6 = d.primary_ip
d.save()
class Migration(migrations.Migration):
replaces = [('dcim', '0003_auto_20160628_1721'), ('dcim', '0004_auto_20160701_2049'), ('dcim', '0005_auto_20160706_1722'), ('dcim', '0006_add_device_primary_ip4_ip6'), ('dcim', '0007_device_copy_primary_ip'), ('dcim', '0008_device_remove_primary_ip'), ('dcim', '0009_site_32bit_asn_support'), ('dcim', '0010_devicebay_installed_device_set_null')]
dependencies = [
('ipam', '0001_initial'),
('dcim', '0002_auto_20160622_1821'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200),
),
migrations.AddField(
model_name='devicetype',
name='subdevice_role',
field=models.NullBooleanField(choices=[(None, b'None'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'),
),
migrations.CreateModel(
name='DeviceBayTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30)),
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bay_templates', to='dcim.DeviceType')),
],
options={
'ordering': ['device_type', 'name'],
'unique_together': {('device_type', 'name')},
},
),
migrations.CreateModel(
name='DeviceBay',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name=b'Name')),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bays', to='dcim.Device')),
('installed_device', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_bay', to='dcim.Device')),
],
options={
'ordering': ['device', 'name'],
'unique_together': {('device', 'name')},
},
),
migrations.AddField(
model_name='interface',
name='mac_address',
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'),
),
migrations.AddField(
model_name='device',
name='primary_ip4',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name=b'Primary IPv4'),
),
migrations.AddField(
model_name='device',
name='primary_ip6',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name=b'Primary IPv6'),
),
migrations.RunPython(
code=copy_primary_ip,
),
migrations.RemoveField(
model_name='device',
name='primary_ip',
),
migrations.AlterField(
model_name='site',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
),
migrations.AlterField(
model_name='devicebay',
name='installed_device',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_bay', to='dcim.Device'),
),
]

View File

@ -1,154 +0,0 @@
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
import utilities.fields
COLOR_CONVERSION = {
'teal': '009688',
'green': '4caf50',
'blue': '2196f3',
'purple': '9c27b0',
'yellow': 'ffeb3b',
'orange': 'ff9800',
'red': 'f44336',
'light_gray': 'c0c0c0',
'medium_gray': '9e9e9e',
'dark_gray': '607d8b',
}
def color_names_to_rgb(apps, schema_editor):
RackRole = apps.get_model('dcim', 'RackRole')
DeviceRole = apps.get_model('dcim', 'DeviceRole')
for color_name, color_rgb in COLOR_CONVERSION.items():
RackRole.objects.filter(color=color_name).update(color=color_rgb)
DeviceRole.objects.filter(color=color_name).update(color=color_rgb)
class Migration(migrations.Migration):
replaces = [('dcim', '0011_devicetype_part_number'), ('dcim', '0012_site_rack_device_add_tenant'), ('dcim', '0013_add_interface_form_factors'), ('dcim', '0014_rack_add_type_width'), ('dcim', '0015_rack_add_u_height_validator'), ('dcim', '0016_module_add_manufacturer'), ('dcim', '0017_rack_add_role'), ('dcim', '0018_device_add_asset_tag'), ('dcim', '0019_new_iface_form_factors'), ('dcim', '0020_rack_desc_units'), ('dcim', '0021_add_ff_flexstack'), ('dcim', '0022_color_names_to_rgb')]
dependencies = [
('dcim', '0010_devicebay_installed_device_set_null'),
('tenancy', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='devicetype',
name='part_number',
field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50),
),
migrations.AddField(
model_name='device',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='rack',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='site',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
),
migrations.AddField(
model_name='rack',
name='type',
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'),
),
migrations.AddField(
model_name='rack',
name='width',
field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'),
),
migrations.AlterField(
model_name='rack',
name='u_height',
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'),
),
migrations.AddField(
model_name='module',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'),
),
migrations.CreateModel(
name='RackRole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
('color', models.CharField(choices=[[b'teal', b'Teal'], [b'green', b'Green'], [b'blue', b'Blue'], [b'purple', b'Purple'], [b'yellow', b'Yellow'], [b'orange', b'Orange'], [b'red', b'Red'], [b'light_gray', b'Light Gray'], [b'medium_gray', b'Medium Gray'], [b'dark_gray', b'Dark Gray']], max_length=30)),
],
options={
'ordering': ['name'],
},
),
migrations.AddField(
model_name='rack',
name='role',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'),
),
migrations.AddField(
model_name='device',
name='asset_tag',
field=utilities.fields.NullableCharField(blank=True, help_text=b'A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name=b'Asset tag'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.AddField(
model_name='rack',
name='desc_units',
field=models.BooleanField(default=False, help_text=b'Units are numbered top-to-bottom', verbose_name=b'Descending units'),
),
migrations.AlterField(
model_name='device',
name='position',
field=models.PositiveSmallIntegerField(blank=True, help_text=b'The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.RunPython(
code=color_names_to_rgb,
),
migrations.AlterField(
model_name='devicerole',
name='color',
field=utilities.fields.ColorField(max_length=6),
),
migrations.AlterField(
model_name='rackrole',
name='color',
field=utilities.fields.ColorField(max_length=6),
),
]

View File

@ -1,478 +0,0 @@
import django.contrib.postgres.fields
import django.core.validators
import django.db.models.deletion
import mptt.fields
from django.conf import settings
from django.db import migrations, models
import dcim.fields
import utilities.fields
def copy_site_from_rack(apps, schema_editor):
Device = apps.get_model('dcim', 'Device')
for device in Device.objects.all():
device.site = device.rack.site
device.save()
def rpc_client_to_napalm_driver(apps, schema_editor):
"""
Migrate legacy RPC clients to their respective NAPALM drivers
"""
Platform = apps.get_model('dcim', 'Platform')
Platform.objects.filter(rpc_client='juniper-junos').update(napalm_driver='junos')
Platform.objects.filter(rpc_client='cisco-ios').update(napalm_driver='ios')
class Migration(migrations.Migration):
replaces = [('dcim', '0023_devicetype_comments'), ('dcim', '0024_site_add_contact_fields'), ('dcim', '0025_devicetype_add_interface_ordering'), ('dcim', '0026_add_rack_reservations'), ('dcim', '0027_device_add_site'), ('dcim', '0028_device_copy_rack_to_site'), ('dcim', '0029_allow_rackless_devices'), ('dcim', '0030_interface_add_lag'), ('dcim', '0031_regions'), ('dcim', '0032_device_increase_name_length'), ('dcim', '0033_rackreservation_rack_editable'), ('dcim', '0034_rename_module_to_inventoryitem'), ('dcim', '0035_device_expand_status_choices'), ('dcim', '0036_add_ff_juniper_vcp'), ('dcim', '0037_unicode_literals'), ('dcim', '0038_wireless_interfaces'), ('dcim', '0039_interface_add_enabled_mtu'), ('dcim', '0040_inventoryitem_add_asset_tag_description'), ('dcim', '0041_napalm_integration'), ('dcim', '0042_interface_ff_10ge_cx4'), ('dcim', '0043_device_component_name_lengths')]
dependencies = [
('dcim', '0022_color_names_to_rgb'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='devicetype',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='site',
name='contact_email',
field=models.EmailField(blank=True, max_length=254, verbose_name=b'Contact E-mail'),
),
migrations.AddField(
model_name='site',
name='contact_name',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='site',
name='contact_phone',
field=models.CharField(blank=True, max_length=20),
),
migrations.AddField(
model_name='devicetype',
name='interface_ordering',
field=models.PositiveSmallIntegerField(choices=[[1, b'Slot/position'], [2, b'Name (alphabetically)']], default=1),
),
migrations.CreateModel(
name='RackReservation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
('created', models.DateTimeField(auto_now_add=True)),
('description', models.CharField(max_length=100)),
('rack', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack')),
('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['created'],
},
),
migrations.AddField(
model_name='device',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
),
migrations.RunPython(
code=copy_site_from_rack,
),
migrations.AlterField(
model_name='device',
name='rack',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
),
migrations.AlterField(
model_name='device',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.CreateModel(
name='Region',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(db_index=True, editable=False)),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='site',
name='region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
),
migrations.AlterField(
model_name='device',
name='name',
field=utilities.fields.NullableCharField(blank=True, max_length=64, null=True, unique=True),
),
migrations.AlterField(
model_name='rackreservation',
name='rack',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack'),
),
migrations.RenameModel(
old_name='Module',
new_name='InventoryItem',
),
migrations.AlterField(
model_name='inventoryitem',
name='device',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='dcim.Device'),
),
migrations.AlterField(
model_name='inventoryitem',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.InventoryItem'),
),
migrations.AlterField(
model_name='inventoryitem',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_items', to='dcim.Manufacturer'),
),
migrations.AlterField(
model_name='device',
name='status',
field=models.PositiveIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
),
migrations.AlterField(
model_name='device',
name='status',
field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.AlterField(
model_name='consoleport',
name='connection_status',
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
),
migrations.AlterField(
model_name='consoleport',
name='cs_port',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'),
),
migrations.AlterField(
model_name='device',
name='asset_tag',
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
),
migrations.AlterField(
model_name='device',
name='face',
field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'),
),
migrations.AlterField(
model_name='device',
name='position',
field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'),
),
migrations.AlterField(
model_name='device',
name='primary_ip4',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'),
),
migrations.AlterField(
model_name='device',
name='primary_ip6',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'),
),
migrations.AlterField(
model_name='device',
name='serial',
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
),
migrations.AlterField(
model_name='device',
name='status',
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'),
),
migrations.AlterField(
model_name='devicebay',
name='name',
field=models.CharField(max_length=50, verbose_name='Name'),
),
migrations.AlterField(
model_name='devicetype',
name='interface_ordering',
field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1),
),
migrations.AlterField(
model_name='devicetype',
name='is_console_server',
field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'),
),
migrations.AlterField(
model_name='devicetype',
name='is_full_depth',
field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'),
),
migrations.AlterField(
model_name='devicetype',
name='is_network_device',
field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'),
),
migrations.AlterField(
model_name='devicetype',
name='is_pdu',
field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'),
),
migrations.AlterField(
model_name='devicetype',
name='part_number',
field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50),
),
migrations.AlterField(
model_name='devicetype',
name='subdevice_role',
field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'),
),
migrations.AlterField(
model_name='devicetype',
name='u_height',
field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AddField(
model_name='interface',
name='lag',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'),
),
migrations.AlterField(
model_name='interface',
name='mac_address',
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'),
),
migrations.AlterField(
model_name='interface',
name='mgmt_only',
field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'),
),
migrations.AlterField(
model_name='interfaceconnection',
name='connection_status',
field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='mgmt_only',
field=models.BooleanField(default=False, verbose_name='Management only'),
),
migrations.AlterField(
model_name='inventoryitem',
name='discovered',
field=models.BooleanField(default=False, verbose_name='Discovered'),
),
migrations.AlterField(
model_name='inventoryitem',
name='name',
field=models.CharField(max_length=50, verbose_name='Name'),
),
migrations.AlterField(
model_name='inventoryitem',
name='part_id',
field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'),
),
migrations.AlterField(
model_name='inventoryitem',
name='serial',
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
),
migrations.AlterField(
model_name='platform',
name='rpc_client',
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='RPC client'),
),
migrations.AlterField(
model_name='powerport',
name='connection_status',
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
),
migrations.AlterField(
model_name='rack',
name='desc_units',
field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'),
),
migrations.AlterField(
model_name='rack',
name='facility_id',
field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'),
),
migrations.AlterField(
model_name='rack',
name='type',
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'),
),
migrations.AlterField(
model_name='rack',
name='u_height',
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'),
),
migrations.AlterField(
model_name='rack',
name='width',
field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'),
),
migrations.AlterField(
model_name='site',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
),
migrations.AlterField(
model_name='site',
name='contact_email',
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AddField(
model_name='interface',
name='enabled',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='interface',
name='mtu',
field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU'),
),
migrations.AddField(
model_name='inventoryitem',
name='asset_tag',
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this item', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
),
migrations.AddField(
model_name='inventoryitem',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AlterModelOptions(
name='device',
options={'ordering': ['name'], 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))},
),
migrations.AddField(
model_name='platform',
name='napalm_driver',
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices.', max_length=50, verbose_name='NAPALM driver'),
),
migrations.AlterField(
model_name='platform',
name='rpc_client',
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='Legacy RPC client'),
),
migrations.RunPython(
code=rpc_client_to_napalm_driver,
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='consoleport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleporttemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleserverport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='devicebaytemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='interface',
name='name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='interfacetemplate',
name='name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='poweroutlet',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='powerport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='powerporttemplate',
name='name',
field=models.CharField(max_length=50),
),
]

View File

@ -1,354 +0,0 @@
import django.contrib.postgres.fields.jsonb
import django.core.validators
import django.db.models.deletion
import taggit.managers
import timezone_field.fields
from django.conf import settings
from django.db import migrations, models
import utilities.fields
class Migration(migrations.Migration):
replaces = [('dcim', '0044_virtualization'), ('dcim', '0045_devicerole_vm_role'), ('dcim', '0046_rack_lengthen_facility_id'), ('dcim', '0047_more_100ge_form_factors'), ('dcim', '0048_rack_serial'), ('dcim', '0049_rackreservation_change_user'), ('dcim', '0050_interface_vlan_tagging'), ('dcim', '0051_rackreservation_tenant'), ('dcim', '0052_virtual_chassis'), ('dcim', '0053_platform_manufacturer'), ('dcim', '0054_site_status_timezone_description'), ('dcim', '0055_virtualchassis_ordering'), ('dcim', '0056_django2'), ('dcim', '0057_tags'), ('dcim', '0058_relax_rack_naming_constraints'), ('dcim', '0059_site_latitude_longitude'), ('dcim', '0060_change_logging'), ('dcim', '0061_platform_napalm_args')]
dependencies = [
('virtualization', '0001_virtualization'),
('tenancy', '0003_unicode_literals'),
('ipam', '0020_ipaddress_add_role_carp'),
('dcim', '0043_device_component_name_lengths'),
('taggit', '0002_auto_20150616_2121'),
]
operations = [
migrations.AddField(
model_name='device',
name='cluster',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
),
migrations.AddField(
model_name='interface',
name='virtual_machine',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'),
),
migrations.AlterField(
model_name='interface',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
),
migrations.AddField(
model_name='devicerole',
name='vm_role',
field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'),
),
migrations.AlterField(
model_name='rack',
name='facility_id',
field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AddField(
model_name='rack',
name='serial',
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
),
migrations.AlterField(
model_name='rackreservation',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='interface',
name='mode',
field=models.PositiveSmallIntegerField(blank=True, choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], null=True),
),
migrations.AddField(
model_name='interface',
name='tagged_vlans',
field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'),
),
migrations.AddField(
model_name='rackreservation',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'),
),
migrations.CreateModel(
name='VirtualChassis',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('domain', models.CharField(blank=True, max_length=30)),
('master', models.OneToOneField(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')),
],
options={
'ordering': ['master'],
'verbose_name_plural': 'virtual chassis',
},
),
migrations.AddField(
model_name='device',
name='virtual_chassis',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'),
),
migrations.AddField(
model_name='device',
name='vc_position',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
),
migrations.AddField(
model_name='device',
name='vc_priority',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
),
migrations.AlterUniqueTogether(
name='device',
unique_together={('rack', 'position', 'face'), ('virtual_chassis', 'vc_position')},
),
migrations.AlterField(
model_name='platform',
name='napalm_driver',
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'),
),
migrations.AddField(
model_name='site',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='site',
name='status',
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1),
),
migrations.AddField(
model_name='site',
name='time_zone',
field=timezone_field.fields.TimeZoneField(blank=True),
),
migrations.AlterField(
model_name='virtualchassis',
name='master',
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device'),
),
migrations.AddField(
model_name='interface',
name='untagged_vlan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
),
migrations.AddField(
model_name='platform',
name='manufacturer',
field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='platforms', to='dcim.Manufacturer'),
),
migrations.AddField(
model_name='device',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='devicetype',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='rack',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='site',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='consoleport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='consoleserverport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='devicebay',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='interface',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='inventoryitem',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='poweroutlet',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='powerport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='virtualchassis',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AlterModelOptions(
name='rack',
options={'ordering': ['site', 'group', 'name']},
),
migrations.AlterUniqueTogether(
name='rack',
unique_together={('group', 'name'), ('group', 'facility_id')},
),
migrations.AddField(
model_name='site',
name='latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True),
),
migrations.AddField(
model_name='site',
name='longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='devicerole',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='devicerole',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='devicetype',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='devicetype',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='manufacturer',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='manufacturer',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='platform',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='platform',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rackgroup',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='rackgroup',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rackreservation',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rackrole',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='rackrole',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='region',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='region',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='virtualchassis',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='virtualchassis',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='device',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='device',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='rack',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='rack',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='rackreservation',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='site',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='site',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='platform',
name='napalm_args',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)', null=True, verbose_name='NAPALM arguments'),
),
]

View File

@ -1,124 +0,0 @@
import django.contrib.postgres.fields.jsonb
import django.core.validators
import django.db.models.deletion
import taggit.managers
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('dcim', '0062_interface_mtu'), ('dcim', '0063_device_local_context_data'), ('dcim', '0064_remove_platform_rpc_client'), ('dcim', '0065_front_rear_ports')]
dependencies = [
('taggit', '0002_auto_20150616_2121'),
('dcim', '0061_platform_napalm_args'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='mtu',
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AddField(
model_name='device',
name='local_context_data',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
),
migrations.RemoveField(
model_name='platform',
name='rpc_client',
),
migrations.CreateModel(
name='RearPort',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('type', models.PositiveSmallIntegerField()),
('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
('description', models.CharField(blank=True, max_length=100)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearports', to='dcim.Device')),
('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
],
options={
'ordering': ['device', 'name'],
'unique_together': {('device', 'name')},
},
),
migrations.CreateModel(
name='RearPortTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('type', models.PositiveSmallIntegerField()),
('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearport_templates', to='dcim.DeviceType')),
],
options={
'ordering': ['device_type', 'name'],
'unique_together': {('device_type', 'name')},
},
),
migrations.CreateModel(
name='FrontPortTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('type', models.PositiveSmallIntegerField()),
('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontport_templates', to='dcim.DeviceType')),
('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontport_templates', to='dcim.RearPortTemplate')),
],
options={
'ordering': ['device_type', 'name'],
'unique_together': {('rear_port', 'rear_port_position'), ('device_type', 'name')},
},
),
migrations.CreateModel(
name='FrontPort',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('type', models.PositiveSmallIntegerField()),
('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
('description', models.CharField(blank=True, max_length=100)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.Device')),
('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.RearPort')),
('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
],
options={
'ordering': ['device', 'name'],
'unique_together': {('device', 'name'), ('rear_port', 'rear_port_position')},
},
),
migrations.AlterField(
model_name='consoleporttemplate',
name='device_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleport_templates', to='dcim.DeviceType'),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='device_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverport_templates', to='dcim.DeviceType'),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='device_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlet_templates', to='dcim.DeviceType'),
),
migrations.AlterField(
model_name='powerporttemplate',
name='device_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='powerport_templates', to='dcim.DeviceType'),
),
]

View File

@ -1,146 +0,0 @@
import taggit.managers
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('dcim', '0067_device_type_remove_qualifiers'), ('dcim', '0068_rack_new_fields'), ('dcim', '0069_deprecate_nullablecharfield'), ('dcim', '0070_custom_tag_models')]
dependencies = [
('extras', '0019_tag_taggeditem'),
('dcim', '0066_cables'),
]
operations = [
migrations.RemoveField(
model_name='devicetype',
name='is_console_server',
),
migrations.RemoveField(
model_name='devicetype',
name='is_network_device',
),
migrations.RemoveField(
model_name='devicetype',
name='is_pdu',
),
migrations.RemoveField(
model_name='devicetype',
name='interface_ordering',
),
migrations.AddField(
model_name='rack',
name='status',
field=models.PositiveSmallIntegerField(default=3),
),
migrations.AddField(
model_name='rack',
name='outer_depth',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='rack',
name='outer_unit',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='rack',
name='outer_width',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='device',
name='asset_tag',
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
migrations.AlterField(
model_name='device',
name='name',
field=models.CharField(blank=True, max_length=64, null=True, unique=True),
),
migrations.AlterField(
model_name='inventoryitem',
name='asset_tag',
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
migrations.AddField(
model_name='rack',
name='asset_tag',
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
migrations.AlterField(
model_name='rack',
name='facility_id',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='consoleport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='consoleserverport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='device',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='devicebay',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='devicetype',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='frontport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='interface',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='inventoryitem',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='poweroutlet',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='powerport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='rack',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='rearport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='site',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
migrations.AlterField(
model_name='virtualchassis',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'),
),
]

View File

@ -19,8 +19,7 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
# Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed, # Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed.
# so this can be omitted when squashing in the future.
migrations.RunPython( migrations.RunPython(
code=rack_outer_unit_to_slug code=rack_outer_unit_to_slug
), ),

View File

@ -0,0 +1,28 @@
# Generated by Django 3.0.3 on 2020-02-18 21:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0099_powerfeed_negative_voltage'),
]
operations = [
migrations.AlterField(
model_name='region',
name='level',
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
model_name='region',
name='lft',
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
model_name='region',
name='rght',
field=models.PositiveIntegerField(editable=False),
),
]

View File

@ -0,0 +1,43 @@
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0100_mptt_remove_indexes'),
]
operations = [
migrations.AddField(
model_name='rackgroup',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.RackGroup'),
),
migrations.AddField(
model_name='rackgroup',
name='level',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='rackgroup',
name='lft',
field=models.PositiveIntegerField(default=1, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='rackgroup',
name='rght',
field=models.PositiveIntegerField(default=2, editable=False),
preserve_default=False,
),
# tree_id will be set to a valid value during the following migration (which needs to be a separate migration)
migrations.AddField(
model_name='rackgroup',
name='tree_id',
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
preserve_default=False,
),
]

View File

@ -0,0 +1,21 @@
from django.db import migrations
def rebuild_mptt(apps, schema_editor):
RackGroup = apps.get_model('dcim', 'RackGroup')
for i, rackgroup in enumerate(RackGroup.objects.all(), start=1):
RackGroup.objects.filter(pk=rackgroup.pk).update(tree_id=i)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0101_nested_rackgroups'),
]
operations = [
migrations.RunPython(
code=rebuild_mptt,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,98 @@
# Generated by Django 3.0.3 on 2020-03-13 20:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0102_nested_rackgroups_rebuild'),
]
operations = [
migrations.AddField(
model_name='manufacturer',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='platform',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='rackgroup',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='region',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='consoleport',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='consoleserverport',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='devicebay',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='devicerole',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='frontport',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='interface',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='inventoryitem',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='poweroutlet',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='powerport',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='rackreservation',
name='description',
field=models.CharField(max_length=200),
),
migrations.AlterField(
model_name='rackrole',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='rearport',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='site',
name='description',
field=models.CharField(blank=True, max_length=200),
),
]

View File

@ -0,0 +1,34 @@
from django.db import migrations
INFINIBAND_SLUGS = (
('inifiband-sdr', 'infiniband-sdr'),
('inifiband-ddr', 'infiniband-ddr'),
('inifiband-qdr', 'infiniband-qdr'),
('inifiband-fdr10', 'infiniband-fdr10'),
('inifiband-fdr', 'infiniband-fdr'),
('inifiband-edr', 'infiniband-edr'),
('inifiband-hdr', 'infiniband-hdr'),
('inifiband-ndr', 'infiniband-ndr'),
('inifiband-xdr', 'infiniband-xdr'),
)
def correct_infiniband_types(apps, schema_editor):
Interface = apps.get_model('dcim', 'Interface')
for old, new in INFINIBAND_SLUGS:
Interface.objects.filter(type=old).update(type=new)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0103_standardize_description'),
]
operations = [
migrations.RunPython(
code=correct_infiniband_types,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-04-21 20:13
from django.db import migrations
import utilities.query_functions
class Migration(migrations.Migration):
dependencies = [
('dcim', '0104_correct_infiniband_types'),
]
operations = [
migrations.AlterModelOptions(
name='interface',
options={'ordering': ('device', utilities.query_functions.CollateAsChar('_name'))},
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.0.6 on 2020-05-26 13:33
from django.db import migrations
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0105_interface_name_collation'),
]
operations = [
migrations.AlterField(
model_name='devicerole',
name='color',
field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
),
migrations.AlterField(
model_name='rackrole',
name='color',
field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
),
]

View File

@ -12,6 +12,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count, F, ProtectedError, Sum from django.db.models import Count, F, ProtectedError, Sum
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from timezone_field import TimeZoneField from timezone_field import TimeZoneField
@ -21,6 +22,8 @@ from dcim.constants import *
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.elevations import RackElevationSVG from dcim.elevations import RackElevationSVG
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_features
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object, to_meters from utilities.utils import serialize_object, to_meters
@ -75,6 +78,7 @@ __all__ = (
# Regions # Regions
# #
@extras_features('custom_fields', 'export_templates', 'webhooks')
class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel): class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel):
""" """
Sites can be grouped within geographic Regions. Sites can be grouped within geographic Regions.
@ -94,13 +98,19 @@ class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel):
slug = models.SlugField( slug = models.SlugField(
unique=True unique=True
) )
custom_field_values = GenericRelation( custom_field_values = GenericRelation(
to='extras.CustomFieldValue', to='extras.CustomFieldValue',
content_type_field='obj_type', content_type_field='obj_type',
object_id_field='obj_id' object_id_field='obj_id'
) )
csv_headers = ['name', 'slug', 'parent'] description = models.CharField(
max_length=200,
blank=True
)
csv_headers = ['name', 'slug', 'parent', 'description']
class MPTTMeta: class MPTTMeta:
order_insertion_by = ['name'] order_insertion_by = ['name']
@ -116,6 +126,7 @@ class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel):
self.name, self.name,
self.slug, self.slug,
self.parent.name if self.parent else None, self.parent.name if self.parent else None,
self.description,
) )
def get_site_count(self): def get_site_count(self):
@ -138,6 +149,7 @@ class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel):
# Sites # Sites
# #
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
class Site(ChangeLoggedModel, CustomFieldModel): class Site(ChangeLoggedModel, CustomFieldModel):
""" """
A Site represents a geographic location within a network; typically a building or campus. The optional facility A Site represents a geographic location within a network; typically a building or campus. The optional facility
@ -176,18 +188,20 @@ class Site(ChangeLoggedModel, CustomFieldModel):
) )
facility = models.CharField( facility = models.CharField(
max_length=50, max_length=50,
blank=True blank=True,
help_text='Local facility ID or description'
) )
asn = ASNField( asn = ASNField(
blank=True, blank=True,
null=True, null=True,
verbose_name='ASN' verbose_name='ASN',
help_text='32-bit autonomous system number'
) )
time_zone = TimeZoneField( time_zone = TimeZoneField(
blank=True blank=True
) )
description = models.CharField( description = models.CharField(
max_length=100, max_length=200,
blank=True blank=True
) )
physical_address = models.CharField( physical_address = models.CharField(
@ -202,13 +216,15 @@ class Site(ChangeLoggedModel, CustomFieldModel):
max_digits=8, max_digits=8,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
null=True null=True,
help_text='GPS coordinate (latitude)'
) )
longitude = models.DecimalField( longitude = models.DecimalField(
max_digits=9, max_digits=9,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
null=True null=True,
help_text='GPS coordinate (longitude)'
) )
contact_name = models.CharField( contact_name = models.CharField(
max_length=50, max_length=50,
@ -288,7 +304,8 @@ class Site(ChangeLoggedModel, CustomFieldModel):
# Racks # Racks
# #
class RackGroup(ChangeLoggedModel): @extras_features('export_templates')
class RackGroup(MPTTModel, ChangeLoggedModel):
""" """
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
@ -303,8 +320,20 @@ class RackGroup(ChangeLoggedModel):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='rack_groups' related_name='rack_groups'
) )
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
description = models.CharField(
max_length=200,
blank=True
)
csv_headers = ['site', 'name', 'slug'] csv_headers = ['site', 'parent', 'name', 'slug', 'description']
class Meta: class Meta:
ordering = ['site', 'name'] ordering = ['site', 'name']
@ -313,6 +342,9 @@ class RackGroup(ChangeLoggedModel):
['site', 'slug'], ['site', 'slug'],
] ]
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self): def __str__(self):
return self.name return self.name
@ -322,10 +354,27 @@ class RackGroup(ChangeLoggedModel):
def to_csv(self): def to_csv(self):
return ( return (
self.site, self.site,
self.parent.name if self.parent else '',
self.name, self.name,
self.slug, self.slug,
self.description,
) )
def to_objectchange(self, action):
# Remove MPTT-internal fields
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)
def clean(self):
# Parent RackGroup (if any) must belong to the same Site
if self.parent and self.parent.site != self.site:
raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")
class RackRole(ChangeLoggedModel): class RackRole(ChangeLoggedModel):
""" """
@ -338,9 +387,11 @@ class RackRole(ChangeLoggedModel):
slug = models.SlugField( slug = models.SlugField(
unique=True unique=True
) )
color = ColorField() color = ColorField(
default=ColorChoices.COLOR_GREY
)
description = models.CharField( description = models.CharField(
max_length=100, max_length=200,
blank=True, blank=True,
) )
@ -364,6 +415,7 @@ class RackRole(ChangeLoggedModel):
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Rack(ChangeLoggedModel, CustomFieldModel): class Rack(ChangeLoggedModel, CustomFieldModel):
""" """
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
@ -381,7 +433,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
max_length=50, max_length=50,
blank=True, blank=True,
null=True, null=True,
verbose_name='Facility ID' verbose_name='Facility ID',
help_text='Locally-assigned identifier'
) )
site = models.ForeignKey( site = models.ForeignKey(
to='dcim.Site', to='dcim.Site',
@ -393,7 +446,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='racks', related_name='racks',
blank=True, blank=True,
null=True null=True,
help_text='Assigned group'
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
@ -412,7 +466,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='racks', related_name='racks',
blank=True, blank=True,
null=True null=True,
help_text='Functional role'
) )
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
@ -442,7 +497,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
u_height = models.PositiveSmallIntegerField( u_height = models.PositiveSmallIntegerField(
default=RACK_U_HEIGHT_DEFAULT, default=RACK_U_HEIGHT_DEFAULT,
verbose_name='Height (U)', verbose_name='Height (U)',
validators=[MinValueValidator(1), MaxValueValidator(100)] validators=[MinValueValidator(1), MaxValueValidator(100)],
help_text='Height in rack units'
) )
desc_units = models.BooleanField( desc_units = models.BooleanField(
default=False, default=False,
@ -451,11 +507,13 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
) )
outer_width = models.PositiveSmallIntegerField( outer_width = models.PositiveSmallIntegerField(
blank=True, blank=True,
null=True null=True,
help_text='Outer dimension of rack (width)'
) )
outer_depth = models.PositiveSmallIntegerField( outer_depth = models.PositiveSmallIntegerField(
blank=True, blank=True,
null=True null=True,
help_text='Outer dimension of rack (depth)'
) )
outer_unit = models.CharField( outer_unit = models.CharField(
max_length=50, max_length=50,
@ -476,7 +534,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', 'site', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
] ]
clone_fields = [ clone_fields = [
@ -615,7 +673,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
pk=exclude pk=exclude
).filter( ).filter(
rack=self, rack=self,
position__gt=0 position__gt=0,
device_type__u_height__gt=0
).filter( ).filter(
Q(face=face) | Q(device_type__is_full_depth=True) Q(face=face) | Q(device_type__is_full_depth=True)
) )
@ -682,7 +741,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT, unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT, unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT,
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT, legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
include_images=True include_images=True,
base_url=None
): ):
""" """
Return an SVG of the rack elevation Return an SVG of the rack elevation
@ -693,8 +753,9 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
height of the elevation height of the elevation
:param legend_width: Width of the unit legend, in pixels :param legend_width: Width of the unit legend, in pixels
:param include_images: Embed front/rear device images where available :param include_images: Embed front/rear device images where available
:param base_url: Base URL for links and images. If none, URLs will be relative.
""" """
elevation = RackElevationSVG(self, include_images=include_images) elevation = RackElevationSVG(self, include_images=include_images, base_url=base_url)
return elevation.render(face, unit_width, unit_height, legend_width) return elevation.render(face, unit_width, unit_height, legend_width)
@ -739,6 +800,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
return 0 return 0
@extras_features('custom_links', 'export_templates', 'webhooks')
class RackReservation(ChangeLoggedModel): class RackReservation(ChangeLoggedModel):
""" """
One or more reserved units within a Rack. One or more reserved units within a Rack.
@ -763,7 +825,7 @@ class RackReservation(ChangeLoggedModel):
on_delete=models.PROTECT on_delete=models.PROTECT
) )
description = models.CharField( description = models.CharField(
max_length=100 max_length=200
) )
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description'] csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
@ -774,9 +836,12 @@ class RackReservation(ChangeLoggedModel):
def __str__(self): def __str__(self):
return "Reservation for rack {}".format(self.rack) return "Reservation for rack {}".format(self.rack)
def get_absolute_url(self):
return reverse('dcim:rackreservation', args=[self.pk])
def clean(self): def clean(self):
if self.units: if hasattr(self, 'rack') and self.units:
# Validate that all specified units exist in the Rack. # Validate that all specified units exist in the Rack.
invalid_units = [u for u in self.units if u not in self.rack.units] invalid_units = [u for u in self.units if u not in self.rack.units]
@ -825,6 +890,7 @@ class RackReservation(ChangeLoggedModel):
# Device Types # Device Types
# #
@extras_features('export_templates', 'webhooks')
class Manufacturer(ChangeLoggedModel): class Manufacturer(ChangeLoggedModel):
""" """
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@ -836,8 +902,12 @@ class Manufacturer(ChangeLoggedModel):
slug = models.SlugField( slug = models.SlugField(
unique=True unique=True
) )
description = models.CharField(
max_length=200,
blank=True
)
csv_headers = ['name', 'slug'] csv_headers = ['name', 'slug', 'description']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -852,9 +922,11 @@ class Manufacturer(ChangeLoggedModel):
return ( return (
self.name, self.name,
self.slug, self.slug,
self.description
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class DeviceType(ChangeLoggedModel, CustomFieldModel): class DeviceType(ChangeLoggedModel, CustomFieldModel):
""" """
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
@ -1039,17 +1111,32 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have # If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
# room to expand within their racks. This validation will impose a very high performance penalty when there are # room to expand within their racks. This validation will impose a very high performance penalty when there are
# many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence. # many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence.
if self.pk is not None and self.u_height > self._original_u_height: if self.pk and self.u_height > self._original_u_height:
for d in Device.objects.filter(device_type=self, position__isnull=False): for d in Device.objects.filter(device_type=self, position__isnull=False):
face_required = None if self.is_full_depth else d.face face_required = None if self.is_full_depth else d.face
u_available = d.rack.get_available_units(u_height=self.u_height, rack_face=face_required, u_available = d.rack.get_available_units(
exclude=[d.pk]) u_height=self.u_height,
rack_face=face_required,
exclude=[d.pk]
)
if d.position not in u_available: if d.position not in u_available:
raise ValidationError({ raise ValidationError({
'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of " 'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of "
"{}U".format(d, d.rack, self.u_height) "{}U".format(d, d.rack, self.u_height)
}) })
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
elif self.pk and self._original_u_height > 0 and self.u_height == 0:
racked_instance_count = Device.objects.filter(device_type=self, position__isnull=False).count()
if racked_instance_count:
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
raise ValidationError({
'u_height': mark_safe(
f'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
f'mounted within racks.'
)
})
if ( if (
self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
) and self.device_bay_templates.count(): ) and self.device_bay_templates.count():
@ -1113,14 +1200,16 @@ class DeviceRole(ChangeLoggedModel):
slug = models.SlugField( slug = models.SlugField(
unique=True unique=True
) )
color = ColorField() color = ColorField(
default=ColorChoices.COLOR_GREY
)
vm_role = models.BooleanField( vm_role = models.BooleanField(
default=True, default=True,
verbose_name='VM Role', verbose_name='VM Role',
help_text='Virtual machines may be assigned to this role' help_text='Virtual machines may be assigned to this role'
) )
description = models.CharField( description = models.CharField(
max_length=100, max_length=200,
blank=True, blank=True,
) )
@ -1176,8 +1265,12 @@ class Platform(ChangeLoggedModel):
verbose_name='NAPALM arguments', verbose_name='NAPALM arguments',
help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)' help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)'
) )
description = models.CharField(
max_length=200,
blank=True
)
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args'] csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -1195,9 +1288,11 @@ class Platform(ChangeLoggedModel):
self.manufacturer.name if self.manufacturer else None, self.manufacturer.name if self.manufacturer else None,
self.napalm_driver, self.napalm_driver,
self.napalm_args, self.napalm_args,
self.description,
) )
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
""" """
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
@ -1342,7 +1437,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
] ]
clone_fields = [ clone_fields = [
@ -1383,7 +1478,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
# of the uniqueness constraint without manual intervention. # of the uniqueness constraint without manual intervention.
if self.name and self.tenant is None: if self.name and self.tenant is None:
if Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True): if Device.objects.exclude(pk=self.pk).filter(name=self.name, site=self.site, tenant__isnull=True):
raise ValidationError({ raise ValidationError({
'name': 'A device with this name already exists.' 'name': 'A device with this name already exists.'
}) })
@ -1458,24 +1553,30 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
# Validate primary IP addresses # Validate primary IP addresses
vc_interfaces = self.vc_interfaces.all() vc_interfaces = self.vc_interfaces.all()
if self.primary_ip4: if self.primary_ip4:
if self.primary_ip4.family != 4:
raise ValidationError({
'primary_ip4': f"{self.primary_ip4} is not an IPv4 address."
})
if self.primary_ip4.interface in vc_interfaces: if self.primary_ip4.interface in vc_interfaces:
pass pass
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in vc_interfaces: elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in vc_interfaces:
pass pass
else: else:
raise ValidationError({ raise ValidationError({
'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format( 'primary_ip4': f"The specified IP address ({self.primary_ip4}) is not assigned to this device."
self.primary_ip4),
}) })
if self.primary_ip6: if self.primary_ip6:
if self.primary_ip6.family != 6:
raise ValidationError({
'primary_ip6': f"{self.primary_ip6} is not an IPv6 address."
})
if self.primary_ip6.interface in vc_interfaces: if self.primary_ip6.interface in vc_interfaces:
pass pass
elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.interface in vc_interfaces: elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.interface in vc_interfaces:
pass pass
else: else:
raise ValidationError({ raise ValidationError({
'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format( 'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device."
self.primary_ip6),
}) })
# Validate manufacturer/platform # Validate manufacturer/platform
@ -1633,6 +1734,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
# Virtual chassis # Virtual chassis
# #
@extras_features('custom_links', 'export_templates', 'webhooks')
class VirtualChassis(ChangeLoggedModel): class VirtualChassis(ChangeLoggedModel):
""" """
A collection of Devices which operate with a shared control plane (e.g. a switch stack). A collection of Devices which operate with a shared control plane (e.g. a switch stack).
@ -1659,7 +1761,7 @@ class VirtualChassis(ChangeLoggedModel):
return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis' return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis'
def get_absolute_url(self): def get_absolute_url(self):
return self.master.get_absolute_url() return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
def clean(self): def clean(self):
@ -1699,6 +1801,7 @@ class VirtualChassis(ChangeLoggedModel):
# Power # Power
# #
@extras_features('custom_links', 'export_templates', 'webhooks')
class PowerPanel(ChangeLoggedModel): class PowerPanel(ChangeLoggedModel):
""" """
A distribution point for electrical power; e.g. a data center RPP. A distribution point for electrical power; e.g. a data center RPP.
@ -1717,7 +1820,7 @@ class PowerPanel(ChangeLoggedModel):
max_length=50 max_length=50
) )
csv_headers = ['site', 'rack_group_name', 'name'] csv_headers = ['site', 'rack_group', 'name']
class Meta: class Meta:
ordering = ['site', 'name'] ordering = ['site', 'name']
@ -1745,6 +1848,7 @@ class PowerPanel(ChangeLoggedModel):
)) ))
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
""" """
An electrical circuit delivered from a PowerPanel. An electrical circuit delivered from a PowerPanel.
@ -1823,7 +1927,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
'amperage', 'max_utilization', 'comments', 'amperage', 'max_utilization', 'comments',
] ]
clone_fields = [ clone_fields = [
@ -1895,6 +1999,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
@property
def parent(self):
return self.power_panel
def get_type_class(self): def get_type_class(self):
return self.TYPE_CLASS_MAP.get(self.type) return self.TYPE_CLASS_MAP.get(self.type)
@ -1906,6 +2014,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
# Cables # Cables
# #
@extras_features('custom_links', 'export_templates', 'webhooks')
class Cable(ChangeLoggedModel): class Cable(ChangeLoggedModel):
""" """
A physical connection between two endpoints. A physical connection between two endpoints.
@ -2006,6 +2115,20 @@ class Cable(ChangeLoggedModel):
# A copy of the PK to be used by __str__ in case the object is deleted # A copy of the PK to be used by __str__ in case the object is deleted
self._pk = self.pk self._pk = self.pk
@classmethod
def from_db(cls, db, field_names, values):
"""
Cache the original A and B terminations of existing Cable instances for later reference inside clean().
"""
instance = super().from_db(db, field_names, values)
instance._orig_termination_a_type = instance.termination_a_type
instance._orig_termination_a_id = instance.termination_a_id
instance._orig_termination_b_type = instance.termination_b_type
instance._orig_termination_b_id = instance.termination_b_id
return instance
def __str__(self): def __str__(self):
return self.label or '#{}'.format(self._pk) return self.label or '#{}'.format(self._pk)
@ -2034,6 +2157,24 @@ class Cable(ChangeLoggedModel):
'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type) 'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
}) })
# If editing an existing Cable instance, check that neither termination has been modified.
if self.pk:
err_msg = 'Cable termination points may not be modified. Delete and recreate the cable instead.'
if (
self.termination_a_type != self._orig_termination_a_type or
self.termination_a_id != self._orig_termination_a_id
):
raise ValidationError({
'termination_a': err_msg
})
if (
self.termination_b_type != self._orig_termination_b_type or
self.termination_b_id != self._orig_termination_b_id
):
raise ValidationError({
'termination_b': err_msg
})
type_a = self.termination_a_type.model type_a = self.termination_a_type.model
type_b = self.termination_b_type.model type_b = self.termination_b_type.model
@ -2053,23 +2194,29 @@ class Cable(ChangeLoggedModel):
# Check that termination types are compatible # Check that termination types are compatible
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a): if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
raise ValidationError("Incompatible termination types: {} and {}".format(
self.termination_a_type, self.termination_b_type
))
# A component with multiple positions must be connected to a component with an equal number of positions
term_a_positions = getattr(self.termination_a, 'positions', 1)
term_b_positions = getattr(self.termination_b, 'positions', 1)
if term_a_positions != term_b_positions:
raise ValidationError( raise ValidationError(
"{} has {} positions and {} has {}. Both terminations must have the same number of positions.".format( f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
self.termination_a, term_a_positions, self.termination_b, term_b_positions
)
) )
# A RearPort with multiple positions must be connected to a RearPort with an equal number of positions
for term_a, term_b in [
(self.termination_a, self.termination_b),
(self.termination_b, self.termination_a)
]:
if isinstance(term_a, RearPort) and term_a.positions > 1:
if not isinstance(term_b, RearPort):
raise ValidationError(
"Rear ports with multiple positions may only be connected to other rear ports"
)
elif term_a.positions != term_b.positions:
raise ValidationError(
f"{term_a} has {term_a.positions} position(s) but {term_b} has {term_b.positions}. "
f"Both terminations must have the same number of positions."
)
# A termination point cannot be connected to itself # A termination point cannot be connected to itself
if self.termination_a == self.termination_b: if self.termination_a == self.termination_b:
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type)) raise ValidationError(f"Cannot connect {self.termination_a_type} to itself")
# A front port cannot be connected to its corresponding rear port # A front port cannot be connected to its corresponding rear port
if ( if (
@ -2141,26 +2288,3 @@ class Cable(ChangeLoggedModel):
if self.termination_a is None: if self.termination_a is None:
return return
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name] return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
def get_path_endpoints(self):
"""
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
None.
"""
a_path = self.termination_b.trace()
b_path = self.termination_a.trace()
# Determine overall path status (connected or planned)
if self.status == CableStatusChoices.STATUS_CONNECTED:
path_status = True
for segment in a_path[1:] + b_path[1:]:
if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
path_status = False
break
else:
path_status = False
a_endpoint = a_path[-1][2]
b_endpoint = b_path[-1][2]
return a_endpoint, b_endpoint, path_status

View File

@ -1,3 +1,5 @@
import logging
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
@ -8,11 +10,13 @@ from taggit.managers import TaggableManager
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.exceptions import LoopDetected from dcim.exceptions import CableTraceSplit
from dcim.fields import MACAddressField from dcim.fields import MACAddressField
from extras.models import ObjectChange, TaggedItem from extras.models import ObjectChange, TaggedItem
from extras.utils import extras_features
from utilities.fields import NaturalOrderingField from utilities.fields import NaturalOrderingField
from utilities.ordering import naturalize_interface from utilities.ordering import naturalize_interface
from utilities.query_functions import CollateAsChar
from utilities.utils import serialize_object from utilities.utils import serialize_object
from virtualization.choices import VMInterfaceTypeChoices from virtualization.choices import VMInterfaceTypeChoices
@ -33,7 +37,7 @@ __all__ = (
class ComponentModel(models.Model): class ComponentModel(models.Model):
description = models.CharField( description = models.CharField(
max_length=100, max_length=200,
blank=True blank=True
) )
@ -87,74 +91,102 @@ class CableTermination(models.Model):
class Meta: class Meta:
abstract = True abstract = True
def trace(self, position=1, follow_circuits=False, cable_history=None): def trace(self):
""" """
Return a list representing a complete cable path, with each individual segment represented as a three-tuple: Return two items: the traceable portion of a cable path, and the termination points where it splits (if any).
This occurs when the trace is initiated from a midpoint along a path which traverses a RearPort. In cases where
the originating endpoint is unknown, it is not possible to know which corresponding FrontPort to follow.
The path is a list representing a complete cable path, with each individual segment represented as a
three-tuple:
[ [
(termination A, cable, termination B), (termination A, cable, termination B),
(termination C, cable, termination D), (termination C, cable, termination D),
(termination E, cable, termination F) (termination E, cable, termination F)
] ]
""" """
def get_peer_port(termination, position=1, follow_circuits=False): endpoint = self
path = []
position_stack = []
def get_peer_port(termination):
from circuits.models import CircuitTermination from circuits.models import CircuitTermination
# Map a front port to its corresponding rear port # Map a front port to its corresponding rear port
if isinstance(termination, FrontPort): if isinstance(termination, FrontPort):
return termination.rear_port, termination.rear_port_position position_stack.append(termination.rear_port_position)
# Retrieve the corresponding RearPort from database to ensure we have an up-to-date instance
peer_port = RearPort.objects.get(pk=termination.rear_port.pk)
return peer_port
# Map a rear port/position to its corresponding front port # Map a rear port/position to its corresponding front port
elif isinstance(termination, RearPort): elif isinstance(termination, RearPort):
# Can't map to a FrontPort without a position if there are multiple options
if termination.positions > 1 and not position_stack:
raise CableTraceSplit(termination)
# We can assume position 1 if the RearPort has only one position
position = position_stack.pop() if position_stack else 1
# Validate the position
if position not in range(1, termination.positions + 1): if position not in range(1, termination.positions + 1):
raise Exception("Invalid position for {} ({} positions): {})".format( raise Exception("Invalid position for {} ({} positions): {})".format(
termination, termination.positions, position termination, termination.positions, position
)) ))
try: try:
peer_port = FrontPort.objects.get( peer_port = FrontPort.objects.get(
rear_port=termination, rear_port=termination,
rear_port_position=position, rear_port_position=position,
) )
return peer_port, 1 return peer_port
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None, None return None
# Follow a circuit to its other termination # Follow a circuit to its other termination
elif isinstance(termination, CircuitTermination) and follow_circuits: elif isinstance(termination, CircuitTermination):
peer_termination = termination.get_peer_termination() peer_termination = termination.get_peer_termination()
if peer_termination is None: if peer_termination is None:
return None, None return None
return peer_termination, position return peer_termination
# Termination is not a pass-through port # Termination is not a pass-through port
else: else:
return None, None return None
if not self.cable: logger = logging.getLogger('netbox.dcim.cable.trace')
return [(self, None, None)] logger.debug("Tracing cable from {} {}".format(self.parent, self))
# Record cable history to detect loops while endpoint is not None:
if cable_history is None:
cable_history = []
elif self.cable in cable_history:
raise LoopDetected()
cable_history.append(self.cable)
far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a # No cable connected; nothing to trace
path = [(self, self.cable, far_end)] if not endpoint.cable:
path.append((endpoint, None, None))
logger.debug("No cable connected")
return path, None
peer_port, position = get_peer_port(far_end, position, follow_circuits) # Check for loops
if peer_port is None: if endpoint.cable in [segment[1] for segment in path]:
return path logger.debug("Loop detected!")
return path, None
try: # Record the current segment in the path
next_segment = peer_port.trace(position, follow_circuits, cable_history) far_end = endpoint.get_cable_peer()
except LoopDetected: path.append((endpoint, endpoint.cable, far_end))
return path logger.debug("{}[{}] --- Cable {} ---> {}[{}]".format(
endpoint.parent, endpoint, endpoint.cable.pk, far_end.parent, far_end
))
if next_segment is None: # Get the peer port of the far end termination
return path + [(peer_port, None, None)] try:
endpoint = get_peer_port(far_end)
except CableTraceSplit as e:
return path, e.termination.frontports.all()
return path + next_segment if endpoint is None:
return path, None
def get_cable_peer(self): def get_cable_peer(self):
if self.cable is None: if self.cable is None:
@ -164,11 +196,29 @@ class CableTermination(models.Model):
if self._cabled_as_b.exists(): if self._cabled_as_b.exists():
return self.cable.termination_a return self.cable.termination_a
def get_path_endpoints(self):
"""
Return all endpoints of paths which traverse this object.
"""
endpoints = []
# Get the far end of the last path segment
path, split_ends = self.trace()
endpoint = path[-1][2]
if split_ends is not None:
for termination in split_ends:
endpoints.extend(termination.get_path_endpoints())
elif endpoint is not None:
endpoints.append(endpoint)
return endpoints
# #
# Console ports # Console ports
# #
@extras_features('export_templates', 'webhooks')
class ConsolePort(CableTermination, ComponentModel): class ConsolePort(CableTermination, ComponentModel):
""" """
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@ -189,7 +239,8 @@ class ConsolePort(CableTermination, ComponentModel):
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
blank=True blank=True,
help_text='Physical port type'
) )
connected_endpoint = models.OneToOneField( connected_endpoint = models.OneToOneField(
to='dcim.ConsoleServerPort', to='dcim.ConsoleServerPort',
@ -229,6 +280,7 @@ class ConsolePort(CableTermination, ComponentModel):
# Console server ports # Console server ports
# #
@extras_features('webhooks')
class ConsoleServerPort(CableTermination, ComponentModel): class ConsoleServerPort(CableTermination, ComponentModel):
""" """
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
@ -249,7 +301,8 @@ class ConsoleServerPort(CableTermination, ComponentModel):
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
blank=True blank=True,
help_text='Physical port type'
) )
connection_status = models.NullBooleanField( connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES, choices=CONNECTION_STATUS_CHOICES,
@ -282,6 +335,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
# Power ports # Power ports
# #
@extras_features('export_templates', 'webhooks')
class PowerPort(CableTermination, ComponentModel): class PowerPort(CableTermination, ComponentModel):
""" """
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@ -302,7 +356,8 @@ class PowerPort(CableTermination, ComponentModel):
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
blank=True blank=True,
help_text='Physical port type'
) )
maximum_draw = models.PositiveSmallIntegerField( maximum_draw = models.PositiveSmallIntegerField(
blank=True, blank=True,
@ -443,6 +498,7 @@ class PowerPort(CableTermination, ComponentModel):
# Power outlets # Power outlets
# #
@extras_features('webhooks')
class PowerOutlet(CableTermination, ComponentModel): class PowerOutlet(CableTermination, ComponentModel):
""" """
A physical power outlet (output) within a Device which provides power to a PowerPort. A physical power outlet (output) within a Device which provides power to a PowerPort.
@ -463,7 +519,8 @@ class PowerOutlet(CableTermination, ComponentModel):
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
blank=True blank=True,
help_text='Physical port type'
) )
power_port = models.ForeignKey( power_port = models.ForeignKey(
to='dcim.PowerPort', to='dcim.PowerPort',
@ -519,6 +576,7 @@ class PowerOutlet(CableTermination, ComponentModel):
# Interfaces # Interfaces
# #
@extras_features('graphs', 'export_templates', 'webhooks')
class Interface(CableTermination, ComponentModel): class Interface(CableTermination, ComponentModel):
""" """
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
@ -599,7 +657,7 @@ class Interface(CableTermination, ComponentModel):
mode = models.CharField( mode = models.CharField(
max_length=50, max_length=50,
choices=InterfaceModeChoices, choices=InterfaceModeChoices,
blank=True, blank=True
) )
untagged_vlan = models.ForeignKey( untagged_vlan = models.ForeignKey(
to='ipam.VLAN', to='ipam.VLAN',
@ -624,7 +682,7 @@ class Interface(CableTermination, ComponentModel):
class Meta: class Meta:
# TODO: ordering and unique_together should include virtual_machine # TODO: ordering and unique_together should include virtual_machine
ordering = ('device', '_name') ordering = ('device', CollateAsChar('_name'))
unique_together = ('device', 'name') unique_together = ('device', 'name')
def __str__(self): def __str__(self):
@ -792,6 +850,7 @@ class Interface(CableTermination, ComponentModel):
# Pass-through ports # Pass-through ports
# #
@extras_features('webhooks')
class FrontPort(CableTermination, ComponentModel): class FrontPort(CableTermination, ComponentModel):
""" """
A pass-through port on the front of a Device. A pass-through port on the front of a Device.
@ -864,6 +923,7 @@ class FrontPort(CableTermination, ComponentModel):
) )
@extras_features('webhooks')
class RearPort(CableTermination, ComponentModel): class RearPort(CableTermination, ComponentModel):
""" """
A pass-through port on the rear of a Device. A pass-through port on the rear of a Device.
@ -915,6 +975,7 @@ class RearPort(CableTermination, ComponentModel):
# Device bays # Device bays
# #
@extras_features('webhooks')
class DeviceBay(ComponentModel): class DeviceBay(ComponentModel):
""" """
An empty space within a Device which can house a child device An empty space within a Device which can house a child device
@ -989,6 +1050,7 @@ class DeviceBay(ComponentModel):
# Inventory items # Inventory items
# #
@extras_features('export_templates', 'webhooks')
class InventoryItem(ComponentModel): class InventoryItem(ComponentModel):
""" """
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
@ -1025,7 +1087,8 @@ class InventoryItem(ComponentModel):
part_id = models.CharField( part_id = models.CharField(
max_length=50, max_length=50,
verbose_name='Part ID', verbose_name='Part ID',
blank=True blank=True,
help_text='Manufacturer-assigned part identifier'
) )
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
@ -1042,7 +1105,7 @@ class InventoryItem(ComponentModel):
) )
discovered = models.BooleanField( discovered = models.BooleanField(
default=False, default=False,
verbose_name='Discovered' help_text='This item was automatically discovered'
) )
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)

View File

@ -1,6 +1,9 @@
import logging
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from .choices import CableStatusChoices
from .models import Cable, Device, VirtualChassis from .models import Cable, Device, VirtualChassis
@ -34,24 +37,40 @@ def update_connected_endpoints(instance, **kwargs):
""" """
When a Cable is saved, check for and update its two connected endpoints When a Cable is saved, check for and update its two connected endpoints
""" """
logger = logging.getLogger('netbox.dcim.cable')
# Cache the Cable on its two termination points # Cache the Cable on its two termination points
if instance.termination_a.cable != instance: if instance.termination_a.cable != instance:
logger.debug("Updating termination A for cable {}".format(instance))
instance.termination_a.cable = instance instance.termination_a.cable = instance
instance.termination_a.save() instance.termination_a.save()
if instance.termination_b.cable != instance: if instance.termination_b.cable != instance:
logger.debug("Updating termination B for cable {}".format(instance))
instance.termination_b.cable = instance instance.termination_b.cable = instance
instance.termination_b.save() instance.termination_b.save()
# Check if this Cable has formed a complete path. If so, update both endpoints. # Update any endpoints for this Cable.
endpoint_a, endpoint_b, path_status = instance.get_path_endpoints() endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False): for endpoint in endpoints:
endpoint_a.connected_endpoint = endpoint_b path, split_ends = endpoint.trace()
endpoint_a.connection_status = path_status # Determine overall path status (connected or planned)
endpoint_a.save() path_status = True
endpoint_b.connected_endpoint = endpoint_a for segment in path:
endpoint_b.connection_status = path_status if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
endpoint_b.save() path_status = False
break
endpoint_a = path[0][0]
endpoint_b = path[-1][2]
if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False):
logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
endpoint_a.connected_endpoint = endpoint_b
endpoint_a.connection_status = path_status
endpoint_a.save()
endpoint_b.connected_endpoint = endpoint_a
endpoint_b.connection_status = path_status
endpoint_b.save()
@receiver(pre_delete, sender=Cable) @receiver(pre_delete, sender=Cable)
@ -59,21 +78,24 @@ def nullify_connected_endpoints(instance, **kwargs):
""" """
When a Cable is deleted, check for and update its two connected endpoints When a Cable is deleted, check for and update its two connected endpoints
""" """
endpoint_a, endpoint_b, _ = instance.get_path_endpoints() logger = logging.getLogger('netbox.dcim.cable')
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
# Disassociate the Cable from its termination points # Disassociate the Cable from its termination points
if instance.termination_a is not None: if instance.termination_a is not None:
logger.debug("Nullifying termination A for cable {}".format(instance))
instance.termination_a.cable = None instance.termination_a.cable = None
instance.termination_a.save() instance.termination_a.save()
if instance.termination_b is not None: if instance.termination_b is not None:
logger.debug("Nullifying termination B for cable {}".format(instance))
instance.termination_b.cable = None instance.termination_b.cable = None
instance.termination_b.save() instance.termination_b.save()
# If this Cable was part of a complete path, tear it down # If this Cable was part of any complete end-to-end paths, tear them down.
if hasattr(endpoint_a, 'connected_endpoint') and hasattr(endpoint_b, 'connected_endpoint'): for endpoint in endpoints:
endpoint_a.connected_endpoint = None logger.debug(f"Removing path information for {endpoint}")
endpoint_a.connection_status = None if hasattr(endpoint, 'connected_endpoint'):
endpoint_a.save() endpoint.connected_endpoint = None
endpoint_b.connected_endpoint = None endpoint.connection_status = None
endpoint_b.connection_status = None endpoint.save()
endpoint_b.save()

View File

@ -2,7 +2,7 @@ import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn from utilities.tables import BaseTable, BooleanColumn, ColorColumn, TagColumn, ToggleColumn
from .models import ( from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@ -11,13 +11,13 @@ from .models import (
VirtualChassis, VirtualChassis,
) )
REGION_LINK = """ MPTT_LINK = """
{% if record.get_children %} {% if record.get_children %}
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i> <span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
{% else %} {% else %}
<span style="padding-left: {{ record.get_ancestors|length }}9px"> <span style="padding-left: {{ record.get_ancestors|length }}9px">
{% endif %} {% endif %}
<a href="{% url 'dcim:site_list' %}?region={{ record.slug }}">{{ record.name }}</a> <a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
</span> </span>
""" """
@ -165,15 +165,6 @@ UTILIZATION_GRAPH = """
{% utilization_graph value %} {% utilization_graph value %}
""" """
VIRTUALCHASSIS_ACTIONS = """
<a href="{% url 'dcim:virtualchassis_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_virtualchassis %}
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
CABLE_TERMINATION_PARENT = """ CABLE_TERMINATION_PARENT = """
{% if value.device %} {% if value.device %}
<a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a> <a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a>
@ -214,9 +205,13 @@ def get_component_template_actions(model_name):
class RegionTable(BaseTable): class RegionTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False) name = tables.TemplateColumn(
site_count = tables.Column(verbose_name='Sites') template_code=MPTT_LINK,
slug = tables.Column(verbose_name='Slug') orderable=False
)
site_count = tables.Column(
verbose_name='Sites'
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=REGION_ACTIONS, template_code=REGION_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -225,7 +220,8 @@ class RegionTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Region model = Region
fields = ('pk', 'name', 'site_count', 'slug', 'actions') fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
# #
@ -234,14 +230,30 @@ class RegionTable(BaseTable):
class SiteTable(BaseTable): class SiteTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(order_by=('_name',)) name = tables.LinkColumn(
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') order_by=('_name',)
region = tables.TemplateColumn(template_code=SITE_REGION_LINK) )
tenant = tables.TemplateColumn(template_code=COL_TENANT) status = tables.TemplateColumn(
template_code=STATUS_LABEL
)
region = tables.TemplateColumn(
template_code=SITE_REGION_LINK
)
tenant = tables.TemplateColumn(
template_code=COL_TENANT
)
tags = TagColumn(
url_name='dcim:site_list'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Site model = Site
fields = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description') fields = (
'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email', 'tags',
)
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
# #
@ -250,7 +262,10 @@ class SiteTable(BaseTable):
class RackGroupTable(BaseTable): class RackGroupTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.TemplateColumn(
template_code=MPTT_LINK,
orderable=False
)
site = tables.LinkColumn( site = tables.LinkColumn(
viewname='dcim:site', viewname='dcim:site',
args=[Accessor('site.slug')], args=[Accessor('site.slug')],
@ -259,7 +274,6 @@ class RackGroupTable(BaseTable):
rack_count = tables.Column( rack_count = tables.Column(
verbose_name='Racks' verbose_name='Racks'
) )
slug = tables.Column()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=RACKGROUP_ACTIONS, template_code=RACKGROUP_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -268,7 +282,8 @@ class RackGroupTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackGroup model = RackGroup
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions') fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions')
default_columns = ('pk', 'name', 'site', 'rack_count', 'description', 'actions')
# #
@ -288,6 +303,7 @@ class RackRoleTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackRole model = RackRole
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions') fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
# #
@ -296,17 +312,34 @@ class RackRoleTable(BaseTable):
class RackTable(BaseTable): class RackTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(order_by=('_name',)) name = tables.LinkColumn(
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) order_by=('_name',)
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') )
tenant = tables.TemplateColumn(template_code=COL_TENANT) site = tables.LinkColumn(
status = tables.TemplateColumn(STATUS_LABEL) viewname='dcim:site',
role = tables.TemplateColumn(RACK_ROLE) args=[Accessor('site.slug')]
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') )
tenant = tables.TemplateColumn(
template_code=COL_TENANT
)
status = tables.TemplateColumn(
template_code=STATUS_LABEL
)
role = tables.TemplateColumn(
template_code=RACK_ROLE
)
u_height = tables.TemplateColumn(
template_code="{{ record.u_height }}U",
verbose_name='Height'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Rack model = Rack
fields = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height') fields = (
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
'width', 'u_height',
)
default_columns = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height')
class RackDetailTable(RackTable): class RackDetailTable(RackTable):
@ -324,9 +357,16 @@ class RackDetailTable(RackTable):
orderable=False, orderable=False,
verbose_name='Power' verbose_name='Power'
) )
tags = TagColumn(
url_name='dcim:rack_list'
)
class Meta(RackTable.Meta): class Meta(RackTable.Meta):
fields = ( fields = (
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
)
default_columns = (
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
'get_utilization', 'get_power_utilization', 'get_utilization', 'get_power_utilization',
) )
@ -338,21 +378,41 @@ class RackDetailTable(RackTable):
class RackReservationTable(BaseTable): class RackReservationTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
reservation = tables.LinkColumn(
viewname='dcim:rackreservation',
args=[Accessor('pk')],
accessor='pk'
)
site = tables.LinkColumn( site = tables.LinkColumn(
viewname='dcim:site', viewname='dcim:site',
accessor=Accessor('rack.site'), accessor=Accessor('rack.site'),
args=[Accessor('rack.site.slug')], args=[Accessor('rack.site.slug')],
) )
tenant = tables.TemplateColumn(template_code=COL_TENANT) tenant = tables.TemplateColumn(
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) template_code=COL_TENANT
unit_list = tables.Column(orderable=False, verbose_name='Units') )
rack = tables.LinkColumn(
viewname='dcim:rack',
args=[Accessor('rack.pk')]
)
unit_list = tables.Column(
orderable=False,
verbose_name='Units'
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' template_code=RACKRESERVATION_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackReservation model = RackReservation
fields = ('pk', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions') fields = (
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions',
)
default_columns = (
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions',
)
# #
@ -380,7 +440,9 @@ class ManufacturerTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Manufacturer model = Manufacturer
fields = ('pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'slug', 'actions') fields = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
)
# #
@ -394,17 +456,25 @@ class DeviceTypeTable(BaseTable):
args=[Accessor('pk')], args=[Accessor('pk')],
verbose_name='Device Type' verbose_name='Device Type'
) )
is_full_depth = BooleanColumn(verbose_name='Full Depth') is_full_depth = BooleanColumn(
verbose_name='Full Depth'
)
instance_count = tables.TemplateColumn( instance_count = tables.TemplateColumn(
template_code=DEVICETYPE_INSTANCES_TEMPLATE, template_code=DEVICETYPE_INSTANCES_TEMPLATE,
verbose_name='Instances' verbose_name='Instances'
) )
tags = TagColumn(
url_name='dcim:devicetype_list'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceType model = DeviceType
fields = ( fields = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
'instance_count', 'instance_count', 'tags',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
) )
@ -414,7 +484,9 @@ class DeviceTypeTable(BaseTable):
class ConsolePortTemplateTable(BaseTable): class ConsolePortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',)) name = tables.Column(
order_by=('_name',)
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('consoleporttemplate'), template_code=get_component_template_actions('consoleporttemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -428,7 +500,10 @@ class ConsolePortTemplateTable(BaseTable):
class ConsolePortImportTable(BaseTable): class ConsolePortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') device = tables.LinkColumn(
viewname='dcim:device',
args=[Accessor('device.pk')]
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsolePort model = ConsolePort
@ -438,7 +513,9 @@ class ConsolePortImportTable(BaseTable):
class ConsoleServerPortTemplateTable(BaseTable): class ConsoleServerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',)) name = tables.Column(
order_by=('_name',)
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('consoleserverporttemplate'), template_code=get_component_template_actions('consoleserverporttemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -452,7 +529,10 @@ class ConsoleServerPortTemplateTable(BaseTable):
class ConsoleServerPortImportTable(BaseTable): class ConsoleServerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') device = tables.LinkColumn(
viewname='dcim:device',
args=[Accessor('device.pk')]
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsoleServerPort model = ConsoleServerPort
@ -462,7 +542,9 @@ class ConsoleServerPortImportTable(BaseTable):
class PowerPortTemplateTable(BaseTable): class PowerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',)) name = tables.Column(
order_by=('_name',)
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('powerporttemplate'), template_code=get_component_template_actions('powerporttemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -476,7 +558,10 @@ class PowerPortTemplateTable(BaseTable):
class PowerPortImportTable(BaseTable): class PowerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') device = tables.LinkColumn(
viewname='dcim:device',
args=[Accessor('device.pk')]
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPort model = PowerPort
@ -486,7 +571,9 @@ class PowerPortImportTable(BaseTable):
class PowerOutletTemplateTable(BaseTable): class PowerOutletTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',)) name = tables.Column(
order_by=('_name',)
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('poweroutlettemplate'), template_code=get_component_template_actions('poweroutlettemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -500,7 +587,10 @@ class PowerOutletTemplateTable(BaseTable):
class PowerOutletImportTable(BaseTable): class PowerOutletImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') device = tables.LinkColumn(
viewname='dcim:device',
args=[Accessor('device.pk')]
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerOutlet model = PowerOutlet
@ -510,7 +600,9 @@ class PowerOutletImportTable(BaseTable):
class InterfaceTemplateTable(BaseTable): class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}") mgmt_only = tables.TemplateColumn(
template_code="{% if value %}OOB Management{% endif %}"
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('interfacetemplate'), template_code=get_component_template_actions('interfacetemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -524,18 +616,30 @@ class InterfaceTemplateTable(BaseTable):
class InterfaceImportTable(BaseTable): class InterfaceImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') device = tables.LinkColumn(
virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine') viewname='dcim:device',
args=[Accessor('device.pk')]
)
virtual_machine = tables.LinkColumn(
viewname='virtualization:virtualmachine',
args=[Accessor('virtual_machine.pk')],
verbose_name='Virtual Machine'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = Interface
fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode') fields = (
'device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu',
'mgmt_only', 'mode',
)
empty_text = False empty_text = False
class FrontPortTemplateTable(BaseTable): class FrontPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',)) name = tables.Column(
order_by=('_name',)
)
rear_port_position = tables.Column( rear_port_position = tables.Column(
verbose_name='Position' verbose_name='Position'
) )
@ -552,7 +656,10 @@ class FrontPortTemplateTable(BaseTable):
class FrontPortImportTable(BaseTable): class FrontPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') device = tables.LinkColumn(
viewname='dcim:device',
args=[Accessor('device.pk')]
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = FrontPort model = FrontPort
@ -562,7 +669,9 @@ class FrontPortImportTable(BaseTable):
class RearPortTemplateTable(BaseTable): class RearPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',)) name = tables.Column(
order_by=('_name',)
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('rearporttemplate'), template_code=get_component_template_actions('rearporttemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -576,7 +685,10 @@ class RearPortTemplateTable(BaseTable):
class RearPortImportTable(BaseTable): class RearPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') device = tables.LinkColumn(
viewname='dcim:device',
args=[Accessor('device.pk')]
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RearPort model = RearPort
@ -586,7 +698,9 @@ class RearPortImportTable(BaseTable):
class DeviceBayTemplateTable(BaseTable): class DeviceBayTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',)) name = tables.Column(
order_by=('_name',)
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('devicebaytemplate'), template_code=get_component_template_actions('devicebaytemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -617,8 +731,10 @@ class DeviceRoleTable(BaseTable):
orderable=False, orderable=False,
verbose_name='VMs' verbose_name='VMs'
) )
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label') color = tables.TemplateColumn(
slug = tables.Column(verbose_name='Slug') template_code=COLOR_LABEL,
verbose_name='Label'
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=DEVICEROLE_ACTIONS, template_code=DEVICEROLE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -628,6 +744,7 @@ class DeviceRoleTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions') fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
# #
@ -656,7 +773,13 @@ class PlatformTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
fields = ('pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'actions') fields = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
'description', 'actions',
)
default_columns = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
)
# #
@ -669,40 +792,99 @@ class DeviceTable(BaseTable):
order_by=('_name',), order_by=('_name',),
template_code=DEVICE_LINK template_code=DEVICE_LINK
) )
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') status = tables.TemplateColumn(
tenant = tables.TemplateColumn(template_code=COL_TENANT) template_code=STATUS_LABEL
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) )
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) tenant = tables.TemplateColumn(
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') template_code=COL_TENANT
)
site = tables.LinkColumn(
viewname='dcim:site',
args=[Accessor('site.slug')]
)
rack = tables.LinkColumn(
viewname='dcim:rack',
args=[Accessor('rack.pk')]
)
device_role = tables.TemplateColumn(
template_code=DEVICE_ROLE,
verbose_name='Role'
)
device_type = tables.LinkColumn( device_type = tables.LinkColumn(
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', viewname='dcim:devicetype',
args=[Accessor('device_type.pk')],
verbose_name='Type',
text=lambda record: record.device_type.display_name text=lambda record: record.device_type.display_name
) )
primary_ip = tables.TemplateColumn(
template_code=DEVICE_PRIMARY_IP,
orderable=False,
verbose_name='IP Address'
)
primary_ip4 = tables.LinkColumn(
viewname='ipam:ipaddress',
args=[Accessor('primary_ip4.pk')],
verbose_name='IPv4 Address'
)
primary_ip6 = tables.LinkColumn(
viewname='ipam:ipaddress',
args=[Accessor('primary_ip6.pk')],
verbose_name='IPv6 Address'
)
cluster = tables.LinkColumn(
viewname='virtualization:cluster',
args=[Accessor('cluster.pk')]
)
virtual_chassis = tables.LinkColumn(
viewname='dcim:virtualchassis',
args=[Accessor('virtual_chassis.pk')]
)
vc_position = tables.Column(
verbose_name='VC Position'
)
vc_priority = tables.Column(
verbose_name='VC Priority'
)
tags = TagColumn(
url_name='dcim:device_list'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Device model = Device
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type') fields = (
'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site',
'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis',
class DeviceDetailTable(DeviceTable): 'vc_position', 'vc_priority', 'tags',
primary_ip = tables.TemplateColumn( )
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP default_columns = (
) 'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip',
)
class Meta(DeviceTable.Meta):
model = Device
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
class DeviceImportTable(BaseTable): class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') name = tables.TemplateColumn(
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') template_code=DEVICE_LINK
tenant = tables.TemplateColumn(template_code=COL_TENANT) )
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') status = tables.TemplateColumn(
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack') template_code=STATUS_LABEL
position = tables.Column(verbose_name='Position') )
device_role = tables.Column(verbose_name='Role') tenant = tables.TemplateColumn(
device_type = tables.Column(verbose_name='Type') template_code=COL_TENANT
)
site = tables.LinkColumn(
viewname='dcim:site',
args=[Accessor('site.slug')]
)
rack = tables.LinkColumn(
viewname='dcim:rack',
args=[Accessor('rack.pk')]
)
device_role = tables.Column(
verbose_name='Role'
)
device_type = tables.Column(
verbose_name='Type'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Device model = Device
@ -840,7 +1022,7 @@ class DeviceBayTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceBay model = DeviceBay
fields = ('name',) fields = ('name', 'description')
class DeviceBayDetailTable(DeviceComponentDetailTable): class DeviceBayDetailTable(DeviceComponentDetailTable):
@ -848,8 +1030,8 @@ class DeviceBayDetailTable(DeviceComponentDetailTable):
installed_device = tables.LinkColumn() installed_device = tables.LinkColumn()
class Meta(DeviceBayTable.Meta): class Meta(DeviceBayTable.Meta):
fields = ('pk', 'name', 'device', 'installed_device') fields = ('pk', 'name', 'device', 'installed_device', 'description')
sequence = ('pk', 'name', 'device', 'installed_device') sequence = ('pk', 'name', 'device', 'installed_device', 'description')
exclude = ('cable',) exclude = ('cable',)
@ -878,23 +1060,23 @@ class CableTable(BaseTable):
template_code=CABLE_TERMINATION_PARENT, template_code=CABLE_TERMINATION_PARENT,
accessor=Accessor('termination_a'), accessor=Accessor('termination_a'),
orderable=False, orderable=False,
verbose_name='Termination A' verbose_name='Side A'
) )
termination_a = tables.LinkColumn( termination_a = tables.LinkColumn(
accessor=Accessor('termination_a'), accessor=Accessor('termination_a'),
orderable=False, orderable=False,
verbose_name='' verbose_name='Termination A'
) )
termination_b_parent = tables.TemplateColumn( termination_b_parent = tables.TemplateColumn(
template_code=CABLE_TERMINATION_PARENT, template_code=CABLE_TERMINATION_PARENT,
accessor=Accessor('termination_b'), accessor=Accessor('termination_b'),
orderable=False, orderable=False,
verbose_name='Termination B' verbose_name='Side B'
) )
termination_b = tables.LinkColumn( termination_b = tables.LinkColumn(
accessor=Accessor('termination_b'), accessor=Accessor('termination_b'),
orderable=False, orderable=False,
verbose_name='' verbose_name='Termination B'
) )
status = tables.TemplateColumn( status = tables.TemplateColumn(
template_code=STATUS_LABEL template_code=STATUS_LABEL
@ -911,6 +1093,10 @@ class CableTable(BaseTable):
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
'status', 'type', 'color', 'length', 'status', 'type', 'color', 'length',
) )
default_columns = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
'status', 'type',
)
# #
@ -978,10 +1164,6 @@ class InterfaceConnectionTable(BaseTable):
args=[Accessor('pk')], args=[Accessor('pk')],
verbose_name='Interface A' verbose_name='Interface A'
) )
description_a = tables.Column(
accessor=Accessor('description'),
verbose_name='Description'
)
device_b = tables.LinkColumn( device_b = tables.LinkColumn(
viewname='dcim:device', viewname='dcim:device',
accessor=Accessor('_connected_interface.device'), accessor=Accessor('_connected_interface.device'),
@ -994,15 +1176,11 @@ class InterfaceConnectionTable(BaseTable):
args=[Accessor('_connected_interface.pk')], args=[Accessor('_connected_interface.pk')],
verbose_name='Interface B' verbose_name='Interface B'
) )
description_b = tables.Column(
accessor=Accessor('_connected_interface.description'),
verbose_name='Description'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = Interface
fields = ( fields = (
'device_a', 'interface_a', 'description_a', 'device_b', 'interface_b', 'description_b', 'connection_status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status',
) )
@ -1012,12 +1190,21 @@ class InterfaceConnectionTable(BaseTable):
class InventoryItemTable(BaseTable): class InventoryItemTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')]) device = tables.LinkColumn(
manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer') viewname='dcim:device_inventory',
args=[Accessor('device.pk')]
)
manufacturer = tables.Column(
accessor=Accessor('manufacturer')
)
discovered = BooleanColumn()
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = InventoryItem model = InventoryItem
fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description') fields = (
'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered'
)
default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag')
# #
@ -1026,17 +1213,21 @@ class InventoryItemTable(BaseTable):
class VirtualChassisTable(BaseTable): class VirtualChassisTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
master = tables.LinkColumn() name = tables.Column(
member_count = tables.Column(verbose_name='Members') accessor=Accessor('master__name'),
actions = tables.TemplateColumn( linkify=True
template_code=VIRTUALCHASSIS_ACTIONS, )
attrs={'td': {'class': 'text-right noprint'}}, member_count = tables.Column(
verbose_name='' verbose_name='Members'
)
tags = TagColumn(
url_name='dcim:virtualchassis_list'
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VirtualChassis model = VirtualChassis
fields = ('pk', 'master', 'domain', 'member_count', 'actions') fields = ('pk', 'name', 'domain', 'member_count', 'tags')
default_columns = ('pk', 'name', 'domain', 'member_count')
# #
@ -1058,6 +1249,7 @@ class PowerPanelTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPanel model = PowerPanel
fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count') fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
# #
@ -1081,7 +1273,22 @@ class PowerFeedTable(BaseTable):
type = tables.TemplateColumn( type = tables.TemplateColumn(
template_code=TYPE_LABEL template_code=TYPE_LABEL
) )
max_utilization = tables.TemplateColumn(
template_code="{{ value }}%"
)
available_power = tables.Column(
verbose_name='Available power (VA)'
)
tags = TagColumn(
url_name='dcim:powerfeed_list'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerFeed model = PowerFeed
fields = ('pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase') fields = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'available_power', 'tags',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
)

View File

@ -4,7 +4,6 @@ from netaddr import IPNetwork
from rest_framework import status from rest_framework import status
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.api import serializers
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import ( from dcim.models import (
@ -15,7 +14,7 @@ from dcim.models import (
) )
from ipam.models import IPAddress, VLAN from ipam.models import IPAddress, VLAN
from extras.models import Graph from extras.models import Graph
from utilities.testing import APITestCase, choices_to_dict from utilities.testing import APITestCase
from virtualization.models import Cluster, ClusterType from virtualization.models import Cluster, ClusterType
@ -28,79 +27,6 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_choices(self):
url = reverse('dcim-api:field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
# Cable
self.assertEqual(choices_to_dict(response.data.get('cable:length_unit')), CableLengthUnitChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('cable:status')), CableStatusChoices.as_dict())
content_types = ContentType.objects.filter(CABLE_TERMINATION_MODELS)
cable_termination_choices = {
"{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types
}
self.assertEqual(choices_to_dict(response.data.get('cable:termination_a_type')), cable_termination_choices)
self.assertEqual(choices_to_dict(response.data.get('cable:termination_b_type')), cable_termination_choices)
self.assertEqual(choices_to_dict(response.data.get('cable:type')), CableTypeChoices.as_dict())
# Console ports
self.assertEqual(choices_to_dict(response.data.get('console-port:type')), ConsolePortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('console-port:connection_status')), dict(CONNECTION_STATUS_CHOICES))
self.assertEqual(choices_to_dict(response.data.get('console-port-template:type')), ConsolePortTypeChoices.as_dict())
# Console server ports
self.assertEqual(choices_to_dict(response.data.get('console-server-port:type')), ConsolePortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('console-server-port-template:type')), ConsolePortTypeChoices.as_dict())
# Device
self.assertEqual(choices_to_dict(response.data.get('device:face')), DeviceFaceChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('device:status')), DeviceStatusChoices.as_dict())
# Device type
self.assertEqual(choices_to_dict(response.data.get('device-type:subdevice_role')), SubdeviceRoleChoices.as_dict())
# Front ports
self.assertEqual(choices_to_dict(response.data.get('front-port:type')), PortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('front-port-template:type')), PortTypeChoices.as_dict())
# Interfaces
self.assertEqual(choices_to_dict(response.data.get('interface:type')), InterfaceTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('interface:mode')), InterfaceModeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('interface-template:type')), InterfaceTypeChoices.as_dict())
# Power feed
self.assertEqual(choices_to_dict(response.data.get('power-feed:phase')), PowerFeedPhaseChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-feed:status')), PowerFeedStatusChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-feed:supply')), PowerFeedSupplyChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-feed:type')), PowerFeedTypeChoices.as_dict())
# Power outlets
self.assertEqual(choices_to_dict(response.data.get('power-outlet:type')), PowerOutletTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-outlet:feed_leg')), PowerOutletFeedLegChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:type')), PowerOutletTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:feed_leg')), PowerOutletFeedLegChoices.as_dict())
# Power ports
self.assertEqual(choices_to_dict(response.data.get('power-port:type')), PowerPortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-port:connection_status')), dict(CONNECTION_STATUS_CHOICES))
self.assertEqual(choices_to_dict(response.data.get('power-port-template:type')), PowerPortTypeChoices.as_dict())
# Rack
self.assertEqual(choices_to_dict(response.data.get('rack:type')), RackTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('rack:width')), RackWidthChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('rack:status')), RackStatusChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('rack:outer_unit')), RackDimensionUnitChoices.as_dict())
# Rear ports
self.assertEqual(choices_to_dict(response.data.get('rear-port:type')), PortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('rear-port-template:type')), PortTypeChoices.as_dict())
# Site
self.assertEqual(choices_to_dict(response.data.get('site:status')), SiteStatusChoices.as_dict())
class RegionTest(APITestCase): class RegionTest(APITestCase):
@ -350,9 +276,11 @@ class RackGroupTest(APITestCase):
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1') 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.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 1', slug='test-rack-group-1') self.parent_rackgroup1 = RackGroup.objects.create(site=self.site1, name='Parent Rack Group 1', slug='parent-rack-group-1')
self.rackgroup2 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 2', slug='test-rack-group-2') self.parent_rackgroup2 = RackGroup.objects.create(site=self.site2, name='Parent Rack Group 2', slug='parent-rack-group-2')
self.rackgroup3 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 3', slug='test-rack-group-3') self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Rack Group 1', slug='rack-group-1', parent=self.parent_rackgroup1)
self.rackgroup2 = RackGroup.objects.create(site=self.site1, name='Rack Group 2', slug='rack-group-2', parent=self.parent_rackgroup1)
self.rackgroup3 = RackGroup.objects.create(site=self.site1, name='Rack Group 3', slug='rack-group-3', parent=self.parent_rackgroup1)
def test_get_rackgroup(self): def test_get_rackgroup(self):
@ -366,7 +294,7 @@ class RackGroupTest(APITestCase):
url = reverse('dcim-api:rackgroup-list') url = reverse('dcim-api:rackgroup-list')
response = self.client.get(url, **self.header) response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3) self.assertEqual(response.data['count'], 5)
def test_list_rackgroups_brief(self): def test_list_rackgroups_brief(self):
@ -381,20 +309,22 @@ class RackGroupTest(APITestCase):
def test_create_rackgroup(self): def test_create_rackgroup(self):
data = { data = {
'name': 'Test Rack Group 4', 'name': 'Rack Group 4',
'slug': 'test-rack-group-4', 'slug': 'rack-group-4',
'site': self.site1.pk, 'site': self.site1.pk,
'parent': self.parent_rackgroup1.pk,
} }
url = reverse('dcim-api:rackgroup-list') url = reverse('dcim-api:rackgroup-list')
response = self.client.post(url, data, format='json', **self.header) response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(RackGroup.objects.count(), 4) self.assertEqual(RackGroup.objects.count(), 6)
rackgroup4 = RackGroup.objects.get(pk=response.data['id']) rackgroup4 = RackGroup.objects.get(pk=response.data['id'])
self.assertEqual(rackgroup4.name, data['name']) self.assertEqual(rackgroup4.name, data['name'])
self.assertEqual(rackgroup4.slug, data['slug']) self.assertEqual(rackgroup4.slug, data['slug'])
self.assertEqual(rackgroup4.site_id, data['site']) self.assertEqual(rackgroup4.site_id, data['site'])
self.assertEqual(rackgroup4.parent_id, data['parent'])
def test_create_rackgroup_bulk(self): def test_create_rackgroup_bulk(self):
@ -403,16 +333,19 @@ class RackGroupTest(APITestCase):
'name': 'Test Rack Group 4', 'name': 'Test Rack Group 4',
'slug': 'test-rack-group-4', 'slug': 'test-rack-group-4',
'site': self.site1.pk, 'site': self.site1.pk,
'parent': self.parent_rackgroup1.pk,
}, },
{ {
'name': 'Test Rack Group 5', 'name': 'Test Rack Group 5',
'slug': 'test-rack-group-5', 'slug': 'test-rack-group-5',
'site': self.site1.pk, 'site': self.site1.pk,
'parent': self.parent_rackgroup1.pk,
}, },
{ {
'name': 'Test Rack Group 6', 'name': 'Test Rack Group 6',
'slug': 'test-rack-group-6', 'slug': 'test-rack-group-6',
'site': self.site1.pk, 'site': self.site1.pk,
'parent': self.parent_rackgroup1.pk,
}, },
] ]
@ -420,7 +353,7 @@ class RackGroupTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header) response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(RackGroup.objects.count(), 6) self.assertEqual(RackGroup.objects.count(), 8)
self.assertEqual(response.data[0]['name'], data[0]['name']) self.assertEqual(response.data[0]['name'], data[0]['name'])
self.assertEqual(response.data[1]['name'], data[1]['name']) self.assertEqual(response.data[1]['name'], data[1]['name'])
self.assertEqual(response.data[2]['name'], data[2]['name']) self.assertEqual(response.data[2]['name'], data[2]['name'])
@ -431,17 +364,19 @@ class RackGroupTest(APITestCase):
'name': 'Test Rack Group X', 'name': 'Test Rack Group X',
'slug': 'test-rack-group-x', 'slug': 'test-rack-group-x',
'site': self.site2.pk, 'site': self.site2.pk,
'parent': self.parent_rackgroup2.pk,
} }
url = reverse('dcim-api:rackgroup-detail', kwargs={'pk': self.rackgroup1.pk}) url = reverse('dcim-api:rackgroup-detail', kwargs={'pk': self.rackgroup1.pk})
response = self.client.put(url, data, format='json', **self.header) response = self.client.put(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK) self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(RackGroup.objects.count(), 3) self.assertEqual(RackGroup.objects.count(), 5)
rackgroup1 = RackGroup.objects.get(pk=response.data['id']) rackgroup1 = RackGroup.objects.get(pk=response.data['id'])
self.assertEqual(rackgroup1.name, data['name']) self.assertEqual(rackgroup1.name, data['name'])
self.assertEqual(rackgroup1.slug, data['slug']) self.assertEqual(rackgroup1.slug, data['slug'])
self.assertEqual(rackgroup1.site_id, data['site']) self.assertEqual(rackgroup1.site_id, data['site'])
self.assertEqual(rackgroup1.parent_id, data['parent'])
def test_delete_rackgroup(self): def test_delete_rackgroup(self):
@ -449,7 +384,7 @@ class RackGroupTest(APITestCase):
response = self.client.delete(url, **self.header) response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(RackGroup.objects.count(), 2) self.assertEqual(RackGroup.objects.count(), 4)
class RackRoleTest(APITestCase): class RackRoleTest(APITestCase):
@ -589,13 +524,6 @@ class RackTest(APITestCase):
self.assertEqual(response.data['name'], self.rack1.name) self.assertEqual(response.data['name'], self.rack1.name)
def test_get_rack_units(self):
url = reverse('dcim-api:rack-units', kwargs={'pk': self.rack1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 42)
def test_get_elevation_rack_units(self): def test_get_elevation_rack_units(self):
url = '{}?q=3'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk})) url = '{}?q=3'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
@ -654,6 +582,7 @@ class RackTest(APITestCase):
data = { data = {
'name': 'Test Rack 4', 'name': 'Test Rack 4',
'facility_id': '1234',
'site': self.site1.pk, 'site': self.site1.pk,
'group': self.rackgroup1.pk, 'group': self.rackgroup1.pk,
'role': self.rackrole1.pk, 'role': self.rackrole1.pk,
@ -1887,6 +1816,7 @@ class DeviceTest(APITestCase):
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1') 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.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
self.rack1 = Rack.objects.create(name='Test Rack 1', site=self.site1, u_height=48)
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype1 = DeviceType.objects.create( self.devicetype1 = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
@ -1992,6 +1922,9 @@ class DeviceTest(APITestCase):
'device_role': self.devicerole1.pk, 'device_role': self.devicerole1.pk,
'name': 'Test Device 4', 'name': 'Test Device 4',
'site': self.site1.pk, 'site': self.site1.pk,
'rack': self.rack1.pk,
'face': DeviceFaceChoices.FACE_FRONT,
'position': 1,
'cluster': self.cluster1.pk, 'cluster': self.cluster1.pk,
} }
@ -2089,6 +2022,20 @@ class DeviceTest(APITestCase):
self.assertFalse('config_context' in response.data['results'][0]) self.assertFalse('config_context' in response.data['results'][0])
def test_unique_name_per_site_constraint(self):
data = {
'device_type': self.devicetype1.pk,
'device_role': self.devicerole1.pk,
'name': 'Test Device 1',
'site': self.site1.pk,
}
url = reverse('dcim-api:device-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
class ConsolePortTest(APITestCase): class ConsolePortTest(APITestCase):

View File

@ -17,14 +17,15 @@ from virtualization.models import Cluster, ClusterType
class RegionTestCase(TestCase): class RegionTestCase(TestCase):
queryset = Region.objects.all() queryset = Region.objects.all()
filterset = RegionFilterSet
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
regions = ( regions = (
Region(name='Region 1', slug='region-1'), Region(name='Region 1', slug='region-1', description='A'),
Region(name='Region 2', slug='region-2'), Region(name='Region 2', slug='region-2', description='B'),
Region(name='Region 3', slug='region-3'), Region(name='Region 3', slug='region-3', description='C'),
) )
for region in regions: for region in regions:
region.save() region.save()
@ -41,24 +42,27 @@ class RegionTestCase(TestCase):
region.save() region.save()
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
params = {'name': ['Region 1', 'Region 2']} params = {'name': ['Region 1', 'Region 2']}
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_slug(self): def test_slug(self):
params = {'slug': ['region-1', 'region-2']} params = {'slug': ['region-1', 'region-2']}
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self): def test_parent(self):
parent_regions = Region.objects.filter(parent__isnull=True)[:2] parent_regions = Region.objects.filter(parent__isnull=True)[:2]
params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]} params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]}
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'parent': [parent_regions[0].slug, parent_regions[1].slug]} params = {'parent': [parent_regions[0].slug, parent_regions[1].slug]}
self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class SiteTestCase(TestCase): class SiteTestCase(TestCase):
@ -81,7 +85,8 @@ class SiteTestCase(TestCase):
TenantGroup(name='Tenant group 2', slug='tenant-group-2'), TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'), TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
) )
TenantGroup.objects.bulk_create(tenant_groups) for tenantgroup in tenant_groups:
tenantgroup.save()
tenants = ( tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
@ -98,8 +103,7 @@ class SiteTestCase(TestCase):
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -138,11 +142,6 @@ class SiteTestCase(TestCase):
params = {'contact_email': ['contact1@example.com', 'contact2@example.com']} params = {'contact_email': ['contact1@example.com', 'contact2@example.com']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_id__in(self):
id_list = self.queryset.values_list('id', flat=True)[:2]
params = {'id__in': ','.join([str(id) for id in id_list])}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_status(self): def test_status(self):
params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]} params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -191,16 +190,24 @@ class RackGroupTestCase(TestCase):
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
rack_groups = ( parent_rack_groups = (
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), RackGroup(name='Parent Rack Group 1', slug='parent-rack-group-1', site=sites[0]),
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), RackGroup(name='Parent Rack Group 2', slug='parent-rack-group-2', site=sites[1]),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), RackGroup(name='Parent Rack Group 3', slug='parent-rack-group-3', site=sites[2]),
) )
RackGroup.objects.bulk_create(rack_groups) for rackgroup in parent_rack_groups:
rackgroup.save()
rack_groups = (
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0], parent=parent_rack_groups[0], description='A'),
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1], parent=parent_rack_groups[1], description='B'),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2], parent=parent_rack_groups[2], description='C'),
)
for rackgroup in rack_groups:
rackgroup.save()
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -211,18 +218,29 @@ class RackGroupTestCase(TestCase):
params = {'slug': ['rack-group-1', 'rack-group-2']} params = {'slug': ['rack-group-1', 'rack-group-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]} params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'region': [regions[0].slug, regions[1].slug]} params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_site(self): def test_site(self):
sites = Site.objects.all()[:2] sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]} params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'site': [sites[0].slug, sites[1].slug]} params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_parent(self):
parent_groups = RackGroup.objects.filter(name__startswith='Parent')[:2]
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -241,8 +259,7 @@ class RackRoleTestCase(TestCase):
RackRole.objects.bulk_create(rack_roles) RackRole.objects.bulk_create(rack_roles)
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -285,7 +302,8 @@ class RackTestCase(TestCase):
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
) )
RackGroup.objects.bulk_create(rack_groups) for rackgroup in rack_groups:
rackgroup.save()
rack_roles = ( rack_roles = (
RackRole(name='Rack Role 1', slug='rack-role-1'), RackRole(name='Rack Role 1', slug='rack-role-1'),
@ -299,7 +317,8 @@ class RackTestCase(TestCase):
TenantGroup(name='Tenant group 2', slug='tenant-group-2'), TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'), TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
) )
TenantGroup.objects.bulk_create(tenant_groups) for tenantgroup in tenant_groups:
tenantgroup.save()
tenants = ( tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
@ -316,8 +335,7 @@ class RackTestCase(TestCase):
Rack.objects.bulk_create(racks) Rack.objects.bulk_create(racks)
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -365,11 +383,6 @@ class RackTestCase(TestCase):
params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER} params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_id__in(self):
id_list = self.queryset.values_list('id', flat=True)[:2]
params = {'id__in': ','.join([str(id) for id in id_list])}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]} params = {'region_id': [regions[0].pk, regions[1].pk]}
@ -442,7 +455,8 @@ class RackReservationTestCase(TestCase):
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
) )
RackGroup.objects.bulk_create(rack_groups) for rackgroup in rack_groups:
rackgroup.save()
racks = ( racks = (
Rack(name='Rack 1', site=sites[0], group=rack_groups[0]), Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
@ -463,7 +477,8 @@ class RackReservationTestCase(TestCase):
TenantGroup(name='Tenant group 2', slug='tenant-group-2'), TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'), TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
) )
TenantGroup.objects.bulk_create(tenant_groups) for tenantgroup in tenant_groups:
tenantgroup.save()
tenants = ( tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
@ -479,9 +494,8 @@ class RackReservationTestCase(TestCase):
) )
RackReservation.objects.bulk_create(reservations) RackReservation.objects.bulk_create(reservations)
def test_id__in(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id__in': ','.join([str(id) for id in id_list])}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self): def test_site(self):
@ -529,15 +543,14 @@ class ManufacturerTestCase(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
manufacturers = ( manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 1', slug='manufacturer-1', description='A'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), Manufacturer(name='Manufacturer 2', slug='manufacturer-2', description='B'),
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), Manufacturer(name='Manufacturer 3', slug='manufacturer-3', description='C'),
) )
Manufacturer.objects.bulk_create(manufacturers) Manufacturer.objects.bulk_create(manufacturers)
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -548,6 +561,10 @@ class ManufacturerTestCase(TestCase):
params = {'slug': ['manufacturer-1', 'manufacturer-2']} params = {'slug': ['manufacturer-1', 'manufacturer-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class DeviceTypeTestCase(TestCase): class DeviceTypeTestCase(TestCase):
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
@ -605,6 +622,10 @@ class DeviceTypeTestCase(TestCase):
DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'), DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'),
)) ))
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_model(self): def test_model(self):
params = {'model': ['Model 1', 'Model 2']} params = {'model': ['Model 1', 'Model 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -631,11 +652,6 @@ class DeviceTypeTestCase(TestCase):
params = {'subdevice_role': SubdeviceRoleChoices.ROLE_PARENT} params = {'subdevice_role': SubdeviceRoleChoices.ROLE_PARENT}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_id__in(self):
id_list = self.queryset.values_list('id', flat=True)[:2]
params = {'id__in': ','.join([str(id) for id in id_list])}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_manufacturer(self): def test_manufacturer(self):
manufacturers = Manufacturer.objects.all()[:2] manufacturers = Manufacturer.objects.all()[:2]
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
@ -709,8 +725,7 @@ class ConsolePortTemplateTestCase(TestCase):
)) ))
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -746,8 +761,7 @@ class ConsoleServerPortTemplateTestCase(TestCase):
)) ))
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -783,8 +797,7 @@ class PowerPortTemplateTestCase(TestCase):
)) ))
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -828,8 +841,7 @@ class PowerOutletTemplateTestCase(TestCase):
)) ))
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -870,8 +882,7 @@ class InterfaceTemplateTestCase(TestCase):
)) ))
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -925,8 +936,7 @@ class FrontPortTemplateTestCase(TestCase):
)) ))
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -967,8 +977,7 @@ class RearPortTemplateTestCase(TestCase):
)) ))
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -1013,8 +1022,7 @@ class DeviceBayTemplateTestCase(TestCase):
)) ))
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -1042,8 +1050,7 @@ class DeviceRoleTestCase(TestCase):
DeviceRole.objects.bulk_create(device_roles) DeviceRole.objects.bulk_create(device_roles)
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -1080,15 +1087,14 @@ class PlatformTestCase(TestCase):
Manufacturer.objects.bulk_create(manufacturers) Manufacturer.objects.bulk_create(manufacturers)
platforms = ( platforms = (
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1'), Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1', description='A'),
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2'), Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2', description='B'),
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3'), Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3', description='C'),
) )
Platform.objects.bulk_create(platforms) Platform.objects.bulk_create(platforms)
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -1099,6 +1105,10 @@ class PlatformTestCase(TestCase):
params = {'slug': ['platform-1', 'platform-2']} params = {'slug': ['platform-1', 'platform-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_napalm_driver(self): def test_napalm_driver(self):
params = {'napalm_driver': ['driver-1', 'driver-2']} params = {'napalm_driver': ['driver-1', 'driver-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -1166,7 +1176,8 @@ class DeviceTestCase(TestCase):
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
) )
RackGroup.objects.bulk_create(rack_groups) for rackgroup in rack_groups:
rackgroup.save()
racks = ( racks = (
Rack(name='Rack 1', site=sites[0], group=rack_groups[0]), Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
@ -1188,7 +1199,8 @@ class DeviceTestCase(TestCase):
TenantGroup(name='Tenant group 2', slug='tenant-group-2'), TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'), TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
) )
TenantGroup.objects.bulk_create(tenant_groups) for tenantgroup in tenant_groups:
tenantgroup.save()
tenants = ( tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
@ -1242,8 +1254,8 @@ class DeviceTestCase(TestCase):
# Assign primary IPs for filtering # Assign primary IPs for filtering
ipaddresses = ( ipaddresses = (
IPAddress(family=4, address='192.0.2.1/24', interface=interfaces[0]), IPAddress(address='192.0.2.1/24', interface=interfaces[0]),
IPAddress(family=4, address='192.0.2.2/24', interface=interfaces[1]), IPAddress(address='192.0.2.2/24', interface=interfaces[1]),
) )
IPAddress.objects.bulk_create(ipaddresses) IPAddress.objects.bulk_create(ipaddresses)
Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0]) Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0])
@ -1255,8 +1267,7 @@ class DeviceTestCase(TestCase):
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2) Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -1283,11 +1294,6 @@ class DeviceTestCase(TestCase):
params = {'vc_priority': [1, 2]} params = {'vc_priority': [1, 2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_id__in(self):
id_list = self.queryset.values_list('id', flat=True)[:2]
params = {'id__in': ','.join([str(id) for id in id_list])}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_manufacturer(self): def test_manufacturer(self):
manufacturers = Manufacturer.objects.all()[:2] manufacturers = Manufacturer.objects.all()[:2]
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
@ -1497,8 +1503,7 @@ class ConsolePortTestCase(TestCase):
# Third port is not connected # Third port is not connected
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -1593,8 +1598,7 @@ class ConsoleServerPortTestCase(TestCase):
# Third port is not connected # Third port is not connected
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -1689,8 +1693,7 @@ class PowerPortTestCase(TestCase):
# Third port is not connected # Third port is not connected
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -1793,8 +1796,7 @@ class PowerOutletTestCase(TestCase):
# Third port is not connected # Third port is not connected
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -1891,9 +1893,8 @@ class InterfaceTestCase(TestCase):
# Third pair is not connected # Third pair is not connected
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:3] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_name(self): def test_name(self):
params = {'name': ['Interface 1', 'Interface 2']} params = {'name': ['Interface 1', 'Interface 2']}
@ -2028,8 +2029,7 @@ class FrontPortTestCase(TestCase):
# Third port is not connected # Third port is not connected
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -2121,8 +2121,7 @@ class RearPortTestCase(TestCase):
# Third port is not connected # Third port is not connected
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -2209,8 +2208,7 @@ class DeviceBayTestCase(TestCase):
DeviceBay.objects.bulk_create(device_bays) DeviceBay.objects.bulk_create(device_bays)
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -2297,8 +2295,7 @@ class InventoryItemTestCase(TestCase):
InventoryItem.objects.bulk_create(child_inventory_items) InventoryItem.objects.bulk_create(child_inventory_items)
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
@ -2409,8 +2406,7 @@ class VirtualChassisTestCase(TestCase):
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[2]) Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[2])
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_domain(self): def test_domain(self):
@ -2498,8 +2494,7 @@ class CableTestCase(TestCase):
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
def test_id(self): def test_id(self):
id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
params = {'id': [str(id) for id in id_list]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self): def test_label(self):
@ -2584,7 +2579,8 @@ class PowerPanelTestCase(TestCase):
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
) )
RackGroup.objects.bulk_create(rack_groups) for rackgroup in rack_groups:
rackgroup.save()
power_panels = ( power_panels = (
PowerPanel(name='Power Panel 1', site=sites[0], rack_group=rack_groups[0]), PowerPanel(name='Power Panel 1', site=sites[0], rack_group=rack_groups[0]),
@ -2593,6 +2589,10 @@ class PowerPanelTestCase(TestCase):
) )
PowerPanel.objects.bulk_create(power_panels) PowerPanel.objects.bulk_create(power_panels)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
params = {'name': ['Power Panel 1', 'Power Panel 2']} params = {'name': ['Power Panel 1', 'Power Panel 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -2660,6 +2660,10 @@ class PowerFeedTestCase(TestCase):
) )
PowerFeed.objects.bulk_create(power_feeds) PowerFeed.objects.bulk_create(power_feeds)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
params = {'name': ['Power Feed 1', 'Power Feed 2']} params = {'name': ['Power Feed 1', 'Power Feed 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -1,6 +1,7 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase from django.test import TestCase
from circuits.models import *
from dcim.choices import * from dcim.choices import *
from dcim.models import * from dcim.models import *
from tenancy.models import Tenant from tenancy.models import Tenant
@ -459,95 +460,494 @@ class CableTestCase(TestCase):
class CablePathTestCase(TestCase): class CablePathTestCase(TestCase):
def setUp(self): @classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Site 1', slug='site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Device Role 1', slug='device-role-1', color='ff0000'
)
self.device1 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
)
self.device2 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Device 2', site=site
)
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
self.panel1 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Panel 1', site=site
)
self.panel2 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
)
self.rear_port1 = RearPort.objects.create(
device=self.panel1, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C
)
self.front_port1 = FrontPort.objects.create(
device=self.panel1, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port1
)
self.rear_port2 = RearPort.objects.create(
device=self.panel2, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C
)
self.front_port2 = FrontPort.objects.create(
device=self.panel2, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port2
) )
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuit = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
CircuitTermination.objects.bulk_create((
CircuitTermination(circuit=circuit, site=site, term_side='A', port_speed=1000),
CircuitTermination(circuit=circuit, site=site, term_side='Z', port_speed=1000),
))
def test_path_completion(self): # Create four network devices with four interfaces each
devices = (
Device(device_type=devicetype, device_role=devicerole, name='Device 1', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Device 2', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Device 3', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Device 4', site=site),
)
Device.objects.bulk_create(devices)
for device in devices:
Interface.objects.bulk_create((
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=device, name='Interface 4', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
))
# First segment # Create four patch panels, each with one rear port and four front ports
cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1) patch_panels = (
Device(device_type=devicetype, device_role=devicerole, name='Panel 1', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 2', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 3', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 4', site=site),
)
Device.objects.bulk_create(patch_panels)
for patch_panel in patch_panels:
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=4, type=PortTypeChoices.TYPE_8P8C)
FrontPort.objects.bulk_create((
FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=patch_panel, name='Front Port 2', rear_port=rearport, rear_port_position=2, type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=patch_panel, name='Front Port 3', rear_port=rearport, rear_port_position=3, type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C),
))
def test_direct_connection(self):
"""
Test a direct connection between two interfaces.
[Device 1] ----- [Device 2]
Iface1 Iface1
"""
# Create cable
cable = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
# Delete cable
cable.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
def test_connection_via_single_rear_port(self):
"""
Test a connection which passes through a single front/rear port pair.
1 2
[Device 1] ----- [Panel 1] ----- [Device 2]
Iface1 FP1 RP1 Iface1
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.save() cable1.save()
interface1 = Interface.objects.get(pk=self.interface1.pk) cable2 = Cable(
self.assertIsNone(interface1.connected_endpoint) termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
self.assertIsNone(interface1.connection_status) termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
# Second segment cable2.save()
cable2 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2)
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
# Delete cable 1
cable1.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
def test_connections_via_patch(self):
"""
Test two connections via patched rear ports:
Device 1 <---> Device 2
Device 3 <---> Device 4
1 2
[Device 1] -----------+ +----------- [Device 2]
Iface1 | | Iface1
FP1 | 3 | FP1
[Panel 1] ----- [Panel 2]
FP2 | RP1 RP1 | FP2
Iface1 | | Iface1
[Device 3] -----------+ +----------- [Device 4]
4 5
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.save()
cable2 = Cable(
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
)
cable2.save() cable2.save()
interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertIsNone(interface1.connected_endpoint)
self.assertIsNone(interface1.connection_status)
# Third segment
cable3 = Cable( cable3 = Cable(
termination_a=self.front_port2, termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_b=self.interface2, termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
status=CableStatusChoices.STATUS_PLANNED
) )
cable3.save() cable3.save()
interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertEqual(interface1.connected_endpoint, self.interface2)
self.assertFalse(interface1.connection_status)
# Switch third segment from planned to connected cable4 = Cable(
cable3.status = CableStatusChoices.STATUS_CONNECTED termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
cable3.save() termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
interface1 = Interface.objects.get(pk=self.interface1.pk) )
self.assertEqual(interface1.connected_endpoint, self.interface2) cable4.save()
self.assertTrue(interface1.connection_status) cable5 = Cable(
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2')
)
cable5.save()
def test_path_teardown(self): # Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
# Build the path # Validate connections
cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1) self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
self.assertTrue(endpoint_c.connection_status)
self.assertTrue(endpoint_d.connection_status)
# Delete cable 3
cable3.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
endpoint_c.refresh_from_db()
endpoint_d.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_c.connected_endpoint)
self.assertIsNone(endpoint_d.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
self.assertIsNone(endpoint_c.connection_status)
self.assertIsNone(endpoint_d.connection_status)
def test_connections_via_multiple_patches(self):
"""
Test two connections via patched rear ports:
Device 1 <---> Device 2
Device 3 <---> Device 4
1 2 3
[Device 1] -----------+ +---------------+ +----------- [Device 2]
Iface1 | | | | Iface1
FP1 | 4 | FP1 FP1 | 5 | FP1
[Panel 1] ----- [Panel 2] [Panel 3] ----- [Panel 4]
FP2 | RP1 RP1 | FP2 FP2 | RP1 RP1 | FP2
Iface1 | | | | Iface1
[Device 3] -----------+ +---------------+ +----------- [Device 4]
6 7 8
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.save() cable1.save()
cable2 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2) cable2 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1')
)
cable2.save() cable2.save()
cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2) cable3 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable3.save() cable3.save()
interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertEqual(interface1.connected_endpoint, self.interface2)
self.assertTrue(interface1.connection_status)
# Remove a cable cable4 = Cable(
cable2.delete() termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
interface1 = Interface.objects.get(pk=self.interface1.pk) termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
self.assertIsNone(interface1.connected_endpoint) )
self.assertIsNone(interface1.connection_status) cable4.save()
interface2 = Interface.objects.get(pk=self.interface2.pk) cable5 = Cable(
self.assertIsNone(interface2.connected_endpoint) termination_a=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1'),
self.assertIsNone(interface2.connection_status) termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
)
cable5.save()
cable6 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
)
cable6.save()
cable7 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 2')
)
cable7.save()
cable8 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
)
cable8.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
self.assertTrue(endpoint_c.connection_status)
self.assertTrue(endpoint_d.connection_status)
# Delete cables 4 and 5
cable4.delete()
cable5.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
endpoint_c.refresh_from_db()
endpoint_d.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_c.connected_endpoint)
self.assertIsNone(endpoint_d.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
self.assertIsNone(endpoint_c.connection_status)
self.assertIsNone(endpoint_d.connection_status)
def test_connections_via_nested_rear_ports(self):
"""
Test two connections via nested rear ports:
Device 1 <---> Device 2
Device 3 <---> Device 4
1 2
[Device 1] -----------+ +----------- [Device 2]
Iface1 | | Iface1
FP1 | 3 4 5 | FP1
[Panel 1] ----- [Panel 2] ----- [Panel 3] ----- [Panel 4]
FP2 | RP1 FP1 RP1 RP1 FP1 RP1 | FP2
Iface1 | | Iface1
[Device 3] -----------+ +----------- [Device 4]
6 7
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.save()
cable2 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable2.save()
cable3 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
)
cable3.save()
cable4 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1')
)
cable4.save()
cable5 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
)
cable5.save()
cable6 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
)
cable6.save()
cable7 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
)
cable7.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
self.assertTrue(endpoint_c.connection_status)
self.assertTrue(endpoint_d.connection_status)
# Delete cable 4
cable4.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
endpoint_c.refresh_from_db()
endpoint_d.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_c.connected_endpoint)
self.assertIsNone(endpoint_d.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
self.assertIsNone(endpoint_c.connection_status)
self.assertIsNone(endpoint_d.connection_status)
def test_connection_via_circuit(self):
"""
1 2
[Device 1] ----- [Circuit] ----- [Device 2]
Iface1 A Z Iface1
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=CircuitTermination.objects.get(term_side='A')
)
cable1.save()
cable2 = Cable(
termination_a=CircuitTermination.objects.get(term_side='Z'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable2.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
# Delete circuit
circuit = Circuit.objects.first().delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
def test_connection_via_patched_circuit(self):
"""
1 2 3 4
[Device 1] ----- [Panel 1] ----- [Circuit] ----- [Panel 2] ----- [Device 2]
Iface1 FP1 RP1 A Z RP1 FP1 Iface1
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.save()
cable2 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_b=CircuitTermination.objects.get(term_side='A')
)
cable2.save()
cable3 = Cable(
termination_a=CircuitTermination.objects.get(term_side='Z'),
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
)
cable3.save()
cable4 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable4.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
# Delete circuit
circuit = Circuit.objects.first().delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)

View File

@ -23,28 +23,34 @@ class NaturalOrderingTestCase(TestCase):
INTERFACES = [ INTERFACES = [
'0', '0',
'0.0',
'0.1', '0.1',
'0.2', '0.2',
'0.10', '0.10',
'0.100', '0.100',
'0:1', '0:1',
'0:1.0',
'0:1.1', '0:1.1',
'0:1.2', '0:1.2',
'0:1.10', '0:1.10',
'0:2', '0:2',
'0:2.0',
'0:2.1', '0:2.1',
'0:2.2', '0:2.2',
'0:2.10', '0:2.10',
'1', '1',
'1.0',
'1.1', '1.1',
'1.2', '1.2',
'1.10', '1.10',
'1.100', '1.100',
'1:1', '1:1',
'1:1.0',
'1:1.1', '1:1.1',
'1:1.2', '1:1.2',
'1:1.10', '1:1.10',
'1:2', '1:2',
'1:2.0',
'1:2.1', '1:2.1',
'1:2.2', '1:2.2',
'1:2.10', '1:2.10',

View File

@ -46,13 +46,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'name': 'Region X', 'name': 'Region X',
'slug': 'region-x', 'slug': 'region-x',
'parent': regions[2].pk, 'parent': regions[2].pk,
'description': 'A new region',
} }
cls.csv_data = ( cls.csv_data = (
"name,slug", "name,slug,description",
"Region 4,region-4", "Region 4,region-4,Fourth region",
"Region 5,region-5", "Region 5,region-5,Fifth region",
"Region 6,region-6", "Region 6,region-6,Sixth region",
) )
@ -122,23 +123,26 @@ class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
site = Site(name='Site 1', slug='site-1') site = Site(name='Site 1', slug='site-1')
site.save() site.save()
RackGroup.objects.bulk_create([ rack_groups = (
RackGroup(name='Rack Group 1', slug='rack-group-1', site=site), RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
RackGroup(name='Rack Group 2', slug='rack-group-2', site=site), RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=site), RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
]) )
for rackgroup in rack_groups:
rackgroup.save()
cls.form_data = { cls.form_data = {
'name': 'Rack Group X', 'name': 'Rack Group X',
'slug': 'rack-group-x', 'slug': 'rack-group-x',
'site': site.pk, 'site': site.pk,
'description': 'A new rack group',
} }
cls.csv_data = ( cls.csv_data = (
"site,name,slug", "site,name,slug,description",
"Site 1,Rack Group 4,rack-group-4", "Site 1,Rack Group 4,rack-group-4,Fourth rack group",
"Site 1,Rack Group 5,rack-group-5", "Site 1,Rack Group 5,rack-group-5,Fifth rack group",
"Site 1,Rack Group 6,rack-group-6", "Site 1,Rack Group 6,rack-group-6,Sixth rack group",
) )
@ -172,10 +176,6 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = RackReservation model = RackReservation
# Disable inapplicable tests
test_get_object = None
test_create_object = None
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -184,7 +184,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
site = Site.objects.create(name='Site 1', slug='site-1') site = Site.objects.create(name='Site 1', slug='site-1')
rack = Rack(name='Rack 1', site=site) rack_group = RackGroup(name='Rack Group 1', slug='rack-group-1', site=site)
rack_group.save()
rack = Rack(name='Rack 1', site=site, group=rack_group)
rack.save() rack.save()
RackReservation.objects.bulk_create([ RackReservation.objects.bulk_create([
@ -202,10 +205,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
'site,rack_name,units,description', 'site,rack_group,rack,units,description',
'Site 1,Rack 1,"10,11,12",Reservation 1', 'Site 1,Rack Group 1,Rack 1,"10,11,12",Reservation 1',
'Site 1,Rack 1,"13,14,15",Reservation 2', 'Site 1,Rack Group 1,Rack 1,"13,14,15",Reservation 2',
'Site 1,Rack 1,"16,17,18",Reservation 3', 'Site 1,Rack Group 1,Rack 1,"16,17,18",Reservation 3',
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
@ -231,7 +234,8 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]) RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1])
) )
RackGroup.objects.bulk_create(rackgroups) for rackgroup in rackgroups:
rackgroup.save()
rackroles = ( rackroles = (
RackRole(name='Rack Role 1', slug='rack-role-1'), RackRole(name='Rack Role 1', slug='rack-role-1'),
@ -267,10 +271,10 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"site,name,width,u_height", "site,group,name,width,u_height",
"Site 1,Rack 4,19,42", "Site 1,,Rack 4,19,42",
"Site 1,Rack 5,19,42", "Site 1,Rack Group 1,Rack 5,19,42",
"Site 1,Rack 6,19,42", "Site 2,Rack Group 2,Rack 6,19,42",
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
@ -306,13 +310,14 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
cls.form_data = { cls.form_data = {
'name': 'Manufacturer X', 'name': 'Manufacturer X',
'slug': 'manufacturer-x', 'slug': 'manufacturer-x',
'description': 'A new manufacturer',
} }
cls.csv_data = ( cls.csv_data = (
"name,slug", "name,slug,description",
"Manufacturer 4,manufacturer-4", "Manufacturer 4,manufacturer-4,Fourth manufacturer",
"Manufacturer 5,manufacturer-5", "Manufacturer 5,manufacturer-5,Fifth manufacturer",
"Manufacturer 6,manufacturer-6", "Manufacturer 6,manufacturer-6,Sixth manufacturer",
) )
@ -361,6 +366,7 @@ manufacturer: Generic
model: TEST-1000 model: TEST-1000
slug: test-1000 slug: test-1000
u_height: 2 u_height: 2
comments: test comment
console-ports: console-ports:
- name: Console Port 1 - name: Console Port 1
type: de-9 type: de-9
@ -451,6 +457,7 @@ device-bays:
self.assertHttpStatus(response, 200) self.assertHttpStatus(response, 200)
dt = DeviceType.objects.get(model='TEST-1000') dt = DeviceType.objects.get(model='TEST-1000')
self.assertEqual(dt.comments, 'test comment')
# Verify all of the components were created # Verify all of the components were created
self.assertEqual(dt.consoleport_templates.count(), 3) self.assertEqual(dt.consoleport_templates.count(), 3)
@ -865,13 +872,14 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'manufacturer': manufacturer.pk, 'manufacturer': manufacturer.pk,
'napalm_driver': 'junos', 'napalm_driver': 'junos',
'napalm_args': None, 'napalm_args': None,
'description': 'A new platform',
} }
cls.csv_data = ( cls.csv_data = (
"name,slug", "name,slug,description",
"Platform 4,platform-4", "Platform 4,platform-4,Fourth platform",
"Platform 5,platform-5", "Platform 5,platform-5,Fifth platform",
"Platform 6,platform-6", "Platform 6,platform-6,Sixth platform",
) )
@ -887,8 +895,11 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
rack_group = RackGroup(site=sites[0], name='Rack Group 1', slug='rack-group-1')
rack_group.save()
racks = ( racks = (
Rack(name='Rack 1', site=sites[0]), Rack(name='Rack 1', site=sites[0], group=rack_group),
Rack(name='Rack 2', site=sites[1]), Rack(name='Rack 2', site=sites[1]),
) )
Rack.objects.bulk_create(racks) Rack.objects.bulk_create(racks)
@ -944,10 +955,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"device_role,manufacturer,model_name,status,site,name", "device_role,manufacturer,device_type,status,name,site,rack_group,rack,position,face",
"Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 4", "Device Role 1,Manufacturer 1,Device Type 1,Active,Device 4,Site 1,Rack Group 1,Rack 1,10,Front",
"Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 5", "Device Role 1,Manufacturer 1,Device Type 1,Active,Device 5,Site 1,Rack Group 1,Rack 1,20,Front",
"Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6", "Device Role 1,Manufacturer 1,Device Type 1,Active,Device 6,Site 1,Rack Group 1,Rack 1,30,Front",
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
@ -1333,37 +1344,37 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase): class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = DeviceBay model = DeviceBay
# Disable inapplicable views
test_bulk_edit_objects = None
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
device1 = create_test_device('Device 1') device = create_test_device('Device 1')
device2 = create_test_device('Device 2')
# Update the DeviceType subdevice role to allow adding DeviceBays # Update the DeviceType subdevice role to allow adding DeviceBays
DeviceType.objects.update(subdevice_role=SubdeviceRoleChoices.ROLE_PARENT) DeviceType.objects.update(subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
DeviceBay.objects.bulk_create([ DeviceBay.objects.bulk_create([
DeviceBay(device=device1, name='Device Bay 1'), DeviceBay(device=device, name='Device Bay 1'),
DeviceBay(device=device1, name='Device Bay 2'), DeviceBay(device=device, name='Device Bay 2'),
DeviceBay(device=device1, name='Device Bay 3'), DeviceBay(device=device, name='Device Bay 3'),
]) ])
cls.form_data = { cls.form_data = {
'device': device2.pk, 'device': device.pk,
'name': 'Device Bay X', 'name': 'Device Bay X',
'description': 'A device bay', 'description': 'A device bay',
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
} }
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device2.pk, 'device': device.pk,
'name_pattern': 'Device Bay [4-6]', 'name_pattern': 'Device Bay [4-6]',
'description': 'A device bay', 'description': 'A device bay',
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
} }
cls.bulk_edit_data = {
'description': 'New description',
}
cls.csv_data = ( cls.csv_data = (
"device,name", "device,name",
"Device 1,Device Bay 4", "Device 1,Device Bay 4",
@ -1504,10 +1515,7 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VirtualChassis model = VirtualChassis
# Disable inapplicable tests # Disable inapplicable tests
test_get_object = None
test_import_objects = None test_import_objects = None
test_bulk_edit_objects = None
test_bulk_delete_objects = None
# TODO: Requires special form handling # TODO: Requires special form handling
test_create_object = None test_create_object = None
@ -1570,7 +1578,8 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
) )
RackGroup.objects.bulk_create(rackgroups) for rackgroup in rackgroups:
rackgroup.save()
PowerPanel.objects.bulk_create(( PowerPanel.objects.bulk_create((
PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 1'), PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 1'),
@ -1585,7 +1594,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"site,rack_group_name,name", "site,rack_group,name",
"Site 1,Rack Group 1,Power Panel 4", "Site 1,Rack Group 1,Power Panel 4",
"Site 1,Rack Group 1,Power Panel 5", "Site 1,Rack Group 1,Power Panel 5",
"Site 1,Rack Group 1,Power Panel 6", "Site 1,Rack Group 1,Power Panel 6",
@ -1644,7 +1653,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"site,panel_name,name,voltage,amperage,max_utilization", "site,power_panel,name,voltage,amperage,max_utilization",
"Site 1,Power Panel 1,Power Feed 4,120,20,80", "Site 1,Power Panel 1,Power Feed 4,120,20,80",
"Site 1,Power Panel 1,Power Feed 5,120,20,80", "Site 1,Power Panel 1,Power Feed 5,120,20,80",
"Site 1,Power Panel 1,Power Feed 6,120,20,80", "Site 1,Power Panel 1,Power Feed 6,120,20,80",

View File

@ -2,7 +2,6 @@ from django.urls import path
from extras.views import ObjectChangeLogView, ImageAttachmentEditView from extras.views import ObjectChangeLogView, ImageAttachmentEditView
from ipam.views import ServiceCreateView from ipam.views import ServiceCreateView
from secrets.views import secret_add
from . import views from . import views
from .models import ( from .models import (
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform, Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
@ -51,9 +50,11 @@ urlpatterns = [
# Rack reservations # Rack reservations
path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
path('rack-reservations/add/', views.RackReservationCreateView.as_view(), name='rackreservation_add'),
path('rack-reservations/import/', views.RackReservationImportView.as_view(), name='rackreservation_import'), path('rack-reservations/import/', views.RackReservationImportView.as_view(), name='rackreservation_import'),
path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
path('rack-reservations/<int:pk>/', views.RackReservationView.as_view(), name='rackreservation'),
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
@ -69,7 +70,6 @@ urlpatterns = [
path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'), path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'), path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
path('racks/<int:rack>/reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
# Manufacturers # Manufacturers
@ -179,7 +179,6 @@ urlpatterns = [
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'), path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'), path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
path('devices/<int:pk>/add-secret/', secret_add, name='device_addsecret'),
path('devices/<int:device>/services/assign/', ServiceCreateView.as_view(), name='device_service_assign'), path('devices/<int:device>/services/assign/', ServiceCreateView.as_view(), name='device_service_assign'),
path('devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), path('devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
@ -279,13 +278,13 @@ urlpatterns = [
path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
# path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
# Device bays # Device bays
path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'), path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
path('device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), path('device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
path('device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'), path('device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
# TODO: Bulk edit view for DeviceBays path('device-bays/edit/', views.DeviceBayBulkEditView.as_view(), name='devicebay_bulk_edit'),
path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
@ -322,6 +321,9 @@ urlpatterns = [
# Virtual chassis # Virtual chassis
path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
path('virtual-chassis/edit/', views.VirtualChassisBulkEditView.as_view(), name='virtualchassis_bulk_edit'),
path('virtual-chassis/delete/', views.VirtualChassisBulkDeleteView.as_view(), name='virtualchassis_bulk_delete'),
path('virtual-chassis/<int:pk>/', views.VirtualChassisView.as_view(), name='virtualchassis'),
path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
path('virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), path('virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),

View File

@ -266,7 +266,13 @@ class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class RackGroupListView(PermissionRequiredMixin, ObjectListView): class RackGroupListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rackgroup' permission_required = 'dcim.view_rackgroup'
queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks')) queryset = RackGroup.objects.add_related_count(
RackGroup.objects.all(),
Rack,
'group',
'rack_count',
cumulative=True
).prefetch_related('site')
filterset = filters.RackGroupFilterSet filterset = filters.RackGroupFilterSet
filterset_form = forms.RackGroupFilterForm filterset_form = forms.RackGroupFilterForm
table = tables.RackGroupTable table = tables.RackGroupTable
@ -473,20 +479,32 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
action_buttons = ('export',) action_buttons = ('export',)
class RackReservationView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_rackreservation'
def get(self, request, pk):
rackreservation = get_object_or_404(RackReservation.objects.prefetch_related('rack'), pk=pk)
return render(request, 'dcim/rackreservation.html', {
'rackreservation': rackreservation,
})
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView): class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackreservation' permission_required = 'dcim.add_rackreservation'
model = RackReservation model = RackReservation
model_form = forms.RackReservationForm model_form = forms.RackReservationForm
template_name = 'dcim/rackreservation_edit.html'
default_return_url = 'dcim:rackreservation_list'
def alter_obj(self, obj, request, args, kwargs): def alter_obj(self, obj, request, args, kwargs):
if not obj.pk: if not obj.pk:
obj.rack = get_object_or_404(Rack, pk=kwargs['rack']) if 'rack' in request.GET:
obj.rack = get_object_or_404(Rack, pk=request.GET.get('rack'))
obj.user = request.user obj.user = request.user
return obj return obj
def get_return_url(self, request, obj):
return obj.rack.get_absolute_url()
class RackReservationEditView(RackReservationCreateView): class RackReservationEditView(RackReservationCreateView):
permission_required = 'dcim.change_rackreservation' permission_required = 'dcim.change_rackreservation'
@ -495,9 +513,7 @@ class RackReservationEditView(RackReservationCreateView):
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_rackreservation' permission_required = 'dcim.delete_rackreservation'
model = RackReservation model = RackReservation
default_return_url = 'dcim:rackreservation_list'
def get_return_url(self, request, obj):
return obj.rack.get_absolute_url()
class RackReservationImportView(PermissionRequiredMixin, BulkImportView): class RackReservationImportView(PermissionRequiredMixin, BulkImportView):
@ -1079,7 +1095,7 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView):
) )
filterset = filters.DeviceFilterSet filterset = filters.DeviceFilterSet
filterset_form = forms.DeviceFilterForm filterset_form = forms.DeviceFilterForm
table = tables.DeviceDetailTable table = tables.DeviceTable
template_name = 'dcim/device_list.html' template_name = 'dcim/device_list.html'
@ -1089,7 +1105,7 @@ class DeviceView(PermissionRequiredMixin, View):
def get(self, request, pk): def get(self, request, pk):
device = get_object_or_404(Device.objects.prefetch_related( device = get_object_or_404(Device.objects.prefetch_related(
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform' 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
), pk=pk) ), pk=pk)
# VirtualChassis members # VirtualChassis members
@ -1883,6 +1899,14 @@ class DeviceBayBulkImportView(PermissionRequiredMixin, BulkImportView):
default_return_url = 'dcim:devicebay_list' default_return_url = 'dcim:devicebay_list'
class DeviceBayBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_devicebay'
queryset = DeviceBay.objects.all()
filterset = filters.DeviceBayFilterSet
table = tables.DeviceBayTable
form = forms.DeviceBayBulkEditForm
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_devicebay' permission_required = 'dcim.change_devicebay'
queryset = DeviceBay.objects.all() queryset = DeviceBay.objects.all()
@ -1905,7 +1929,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV
permission_required = 'dcim.add_consoleport' permission_required = 'dcim.add_consoleport'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
form = forms.DeviceBulkAddComponentForm form = forms.ConsolePortBulkCreateForm
model = ConsolePort model = ConsolePort
model_form = forms.ConsolePortForm model_form = forms.ConsolePortForm
filterset = filters.DeviceFilterSet filterset = filters.DeviceFilterSet
@ -1917,7 +1941,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC
permission_required = 'dcim.add_consoleserverport' permission_required = 'dcim.add_consoleserverport'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
form = forms.DeviceBulkAddComponentForm form = forms.ConsoleServerPortBulkCreateForm
model = ConsoleServerPort model = ConsoleServerPort
model_form = forms.ConsoleServerPortForm model_form = forms.ConsoleServerPortForm
filterset = filters.DeviceFilterSet filterset = filters.DeviceFilterSet
@ -1929,7 +1953,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie
permission_required = 'dcim.add_powerport' permission_required = 'dcim.add_powerport'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
form = forms.DeviceBulkAddComponentForm form = forms.PowerPortBulkCreateForm
model = PowerPort model = PowerPort
model_form = forms.PowerPortForm model_form = forms.PowerPortForm
filterset = filters.DeviceFilterSet filterset = filters.DeviceFilterSet
@ -1941,7 +1965,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV
permission_required = 'dcim.add_poweroutlet' permission_required = 'dcim.add_poweroutlet'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
form = forms.DeviceBulkAddComponentForm form = forms.PowerOutletBulkCreateForm
model = PowerOutlet model = PowerOutlet
model_form = forms.PowerOutletForm model_form = forms.PowerOutletForm
filterset = filters.DeviceFilterSet filterset = filters.DeviceFilterSet
@ -1953,7 +1977,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
permission_required = 'dcim.add_interface' permission_required = 'dcim.add_interface'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
form = forms.DeviceBulkAddInterfaceForm form = forms.InterfaceBulkCreateForm
model = Interface model = Interface
model_form = forms.InterfaceForm model_form = forms.InterfaceForm
filterset = filters.DeviceFilterSet filterset = filters.DeviceFilterSet
@ -1961,11 +1985,35 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
default_return_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
# class DeviceBulkAddFrontPortView(PermissionRequiredMixin, BulkComponentCreateView):
# permission_required = 'dcim.add_frontport'
# parent_model = Device
# parent_field = 'device'
# form = forms.FrontPortBulkCreateForm
# model = FrontPort
# model_form = forms.FrontPortForm
# filterset = filters.DeviceFilterSet
# table = tables.DeviceTable
# default_return_url = 'dcim:device_list'
class DeviceBulkAddRearPortView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_rearport'
parent_model = Device
parent_field = 'device'
form = forms.RearPortBulkCreateForm
model = RearPort
model_form = forms.RearPortForm
filterset = filters.DeviceFilterSet
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateView): class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateView):
permission_required = 'dcim.add_devicebay' permission_required = 'dcim.add_devicebay'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
form = forms.DeviceBulkAddComponentForm form = forms.DeviceBayBulkCreateForm
model = DeviceBay model = DeviceBay
model_form = forms.DeviceBayForm model_form = forms.DeviceBayForm
filterset = filters.DeviceFilterSet filterset = filters.DeviceFilterSet
@ -2009,12 +2057,15 @@ class CableTraceView(PermissionRequiredMixin, View):
def get(self, request, model, pk): def get(self, request, model, pk):
obj = get_object_or_404(model, pk=pk) obj = get_object_or_404(model, pk=pk)
trace = obj.trace(follow_circuits=True) path, split_ends = obj.trace()
total_length = sum([entry[1]._abs_length for entry in trace if entry[1] and entry[1]._abs_length]) total_length = sum(
[entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length]
)
return render(request, 'dcim/cable_trace.html', { return render(request, 'dcim/cable_trace.html', {
'obj': obj, 'obj': obj,
'trace': trace, 'trace': path,
'split_ends': split_ends,
'total_length': total_length, 'total_length': total_length,
}) })
@ -2227,19 +2278,15 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
csv_data = [ csv_data = [
# Headers # Headers
','.join([ ','.join([
'device_a', 'interface_a', 'interface_a_description', 'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status'
'device_b', 'interface_b', 'interface_b_description',
'connection_status'
]) ])
] ]
for obj in self.queryset: for obj in self.queryset:
csv = csv_format([ csv = csv_format([
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None, obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
obj.connected_endpoint.name if obj.connected_endpoint else None, obj.connected_endpoint.name if obj.connected_endpoint else None,
obj.connected_endpoint.description if obj.connected_endpoint else None,
obj.device.identifier, obj.device.identifier,
obj.name, obj.name,
obj.description,
obj.get_connection_status_display(), obj.get_connection_status_display(),
]) ])
csv_data.append(csv) csv_data.append(csv)
@ -2316,6 +2363,17 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
action_buttons = ('export',) action_buttons = ('export',)
class VirtualChassisView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_virtualchassis'
def get(self, request, pk):
virtualchassis = get_object_or_404(VirtualChassis.objects.prefetch_related('members'), pk=pk)
return render(request, 'dcim/virtualchassis.html', {
'virtualchassis': virtualchassis,
})
class VirtualChassisCreateView(PermissionRequiredMixin, View): class VirtualChassisCreateView(PermissionRequiredMixin, View):
permission_required = 'dcim.add_virtualchassis' permission_required = 'dcim.add_virtualchassis'
@ -2543,6 +2601,23 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
}) })
class VirtualChassisBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_virtualchassis'
queryset = VirtualChassis.objects.all()
filterset = filters.VirtualChassisFilterSet
table = tables.VirtualChassisTable
form = forms.VirtualChassisBulkEditForm
default_return_url = 'dcim:virtualchassis_list'
class VirtualChassisBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_virtualchassis'
queryset = VirtualChassis.objects.all()
filterset = filters.VirtualChassisFilterSet
table = tables.VirtualChassisTable
default_return_url = 'dcim:virtualchassis_list'
# #
# Power panels # Power panels
# #

View File

@ -1,7 +1,6 @@
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from netbox.admin import admin_site
from utilities.forms import LaxURLField from utilities.forms import LaxURLField
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, Webhook from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, Webhook
from .reports import get_report from .reports import get_report
@ -35,7 +34,7 @@ class WebhookForm(forms.ModelForm):
order_content_types(self.fields['obj_type']) order_content_types(self.fields['obj_type'])
@admin.register(Webhook, site=admin_site) @admin.register(Webhook)
class WebhookAdmin(admin.ModelAdmin): class WebhookAdmin(admin.ModelAdmin):
list_display = [ list_display = [
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete', 'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete',
@ -93,7 +92,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
extra = 5 extra = 5
@admin.register(CustomField, site=admin_site) @admin.register(CustomField)
class CustomFieldAdmin(admin.ModelAdmin): class CustomFieldAdmin(admin.ModelAdmin):
inlines = [CustomFieldChoiceAdmin] inlines = [CustomFieldChoiceAdmin]
list_display = [ list_display = [
@ -135,7 +134,7 @@ class CustomLinkForm(forms.ModelForm):
self.fields['content_type'].choices.insert(0, ('', '---------')) self.fields['content_type'].choices.insert(0, ('', '---------'))
@admin.register(CustomLink, site=admin_site) @admin.register(CustomLink)
class CustomLinkAdmin(admin.ModelAdmin): class CustomLinkAdmin(admin.ModelAdmin):
list_display = [ list_display = [
'name', 'content_type', 'group_name', 'weight', 'name', 'content_type', 'group_name', 'weight',
@ -150,7 +149,7 @@ class CustomLinkAdmin(admin.ModelAdmin):
# Graphs # Graphs
# #
@admin.register(Graph, site=admin_site) @admin.register(Graph)
class GraphAdmin(admin.ModelAdmin): class GraphAdmin(admin.ModelAdmin):
list_display = [ list_display = [
'name', 'type', 'weight', 'template_language', 'source', 'name', 'type', 'weight', 'template_language', 'source',
@ -178,7 +177,7 @@ class ExportTemplateForm(forms.ModelForm):
self.fields['content_type'].choices.insert(0, ('', '---------')) self.fields['content_type'].choices.insert(0, ('', '---------'))
@admin.register(ExportTemplate, site=admin_site) @admin.register(ExportTemplate)
class ExportTemplateAdmin(admin.ModelAdmin): class ExportTemplateAdmin(admin.ModelAdmin):
list_display = [ list_display = [
'name', 'content_type', 'description', 'mime_type', 'file_extension', 'name', 'content_type', 'description', 'mime_type', 'file_extension',
@ -193,7 +192,7 @@ class ExportTemplateAdmin(admin.ModelAdmin):
# Reports # Reports
# #
@admin.register(ReportResult, site=admin_site) @admin.register(ReportResult)
class ReportResultAdmin(admin.ModelAdmin): class ReportResultAdmin(admin.ModelAdmin):
list_display = [ list_display = [
'report', 'active', 'created', 'user', 'passing', 'report', 'active', 'created', 'user', 'passing',

View File

@ -20,7 +20,10 @@ class CustomFieldDefaultValues:
""" """
Return a dictionary of all CustomFields assigned to the parent model and their default values. Return a dictionary of all CustomFields assigned to the parent model and their default values.
""" """
def __call__(self): requires_context = True
def __call__(self, serializer_field):
self.model = serializer_field.parent.Meta.model
# Retrieve the CustomFields for the parent model # Retrieve the CustomFields for the parent model
content_type = ContentType.objects.get_for_model(self.model) content_type = ContentType.objects.get_for_model(self.model)
@ -49,9 +52,6 @@ class CustomFieldDefaultValues:
return value return value
def set_context(self, serializer_field):
self.model = serializer_field.parent.Meta.model
class CustomFieldsSerializer(serializers.BaseSerializer): class CustomFieldsSerializer(serializers.BaseSerializer):

View File

@ -13,6 +13,7 @@ from extras.constants import *
from extras.models import ( from extras.models import (
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
) )
from extras.utils import FeatureQuery
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from users.api.nested_serializers import NestedUserSerializer from users.api.nested_serializers import NestedUserSerializer
@ -31,7 +32,7 @@ from .nested_serializers import *
class GraphSerializer(ValidatedModelSerializer): class GraphSerializer(ValidatedModelSerializer):
type = ContentTypeField( type = ContentTypeField(
queryset=ContentType.objects.filter(GRAPH_MODELS), queryset=ContentType.objects.filter(FeatureQuery('graphs').get_query()),
) )
class Meta: class Meta:
@ -67,7 +68,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
class ExportTemplateSerializer(ValidatedModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer):
content_type = ContentTypeField( content_type = ContentTypeField(
queryset=ContentType.objects.filter(EXPORTTEMPLATE_MODELS), queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
) )
template_language = ChoiceField( template_language = ChoiceField(
choices=TemplateLanguageChoices, choices=TemplateLanguageChoices,
@ -91,7 +92,7 @@ class TagSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Tag model = Tag
fields = ['id', 'name', 'slug', 'color', 'comments', 'tagged_items'] fields = ['id', 'name', 'slug', 'color', 'description', 'tagged_items']
# #

View File

@ -14,9 +14,6 @@ class ExtrasRootView(routers.APIRootView):
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.APIRootView = ExtrasRootView router.APIRootView = ExtrasRootView
# Field choices
router.register('_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
# Custom field choices # Custom field choices
router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice') router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')

View File

@ -15,22 +15,10 @@ from extras.models import (
) )
from extras.reports import get_report, get_reports from extras.reports import get_report, get_reports
from extras.scripts import get_script, get_scripts, run_script from extras.scripts import get_script, get_scripts, run_script
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet
from . import serializers from . import serializers
#
# Field choices
#
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(serializers.ExportTemplateSerializer, ['template_language']),
(serializers.GraphSerializer, ['type', 'template_language']),
(serializers.ObjectChangeSerializer, ['action']),
)
# #
# Custom field choices # Custom field choices
# #

View File

@ -1,131 +1,3 @@
from django.db.models import Q
# Models which support custom fields
CUSTOMFIELD_MODELS = Q(
Q(app_label='circuits', model__in=[
'circuit',
'provider',
]) |
Q(app_label='dcim', model__in=[
'device',
'devicetype',
'powerfeed',
'rack',
'region',
'site',
]) |
Q(app_label='ipam', model__in=[
'aggregate',
'ipaddress',
'prefix',
'service',
'vlan',
'vrf',
]) |
Q(app_label='secrets', model__in=[
'secret',
]) |
Q(app_label='tenancy', model__in=[
'tenant',
]) |
Q(app_label='virtualization', model__in=[
'cluster',
'virtualmachine',
])
)
# Custom links
CUSTOMLINK_MODELS = Q(
Q(app_label='circuits', model__in=[
'circuit',
'provider',
]) |
Q(app_label='dcim', model__in=[
'cable',
'device',
'devicetype',
'powerpanel',
'powerfeed',
'rack',
'region',
'site',
]) |
Q(app_label='ipam', model__in=[
'aggregate',
'ipaddress',
'prefix',
'service',
'vlan',
'vrf',
]) |
Q(app_label='secrets', model__in=[
'secret',
]) |
Q(app_label='tenancy', model__in=[
'tenant',
]) |
Q(app_label='virtualization', model__in=[
'cluster',
'virtualmachine',
])
)
# Models which can have Graphs associated with them
GRAPH_MODELS = Q(
Q(app_label='circuits', model__in=[
'provider',
]) |
Q(app_label='dcim', model__in=[
'device',
'interface',
'site',
])
)
# Models which support export templates
EXPORTTEMPLATE_MODELS = Q(
Q(app_label='circuits', model__in=[
'circuit',
'provider',
]) |
Q(app_label='dcim', model__in=[
'cable',
'consoleport',
'device',
'devicetype',
'interface',
'inventoryitem',
'manufacturer',
'powerpanel',
'powerport',
'powerfeed',
'rack',
'rackgroup',
'region',
'site',
'virtualchassis',
]) |
Q(app_label='ipam', model__in=[
'aggregate',
'ipaddress',
'prefix',
'service',
'vlan',
'vrf',
]) |
Q(app_label='secrets', model__in=[
'secret',
]) |
Q(app_label='tenancy', model__in=[
'tenant',
]) |
Q(app_label='virtualization', model__in=[
'cluster',
'virtualmachine',
])
)
# Report logging levels # Report logging levels
LOG_DEFAULT = 0 LOG_DEFAULT = 0
LOG_SUCCESS = 10 LOG_SUCCESS = 10
@ -140,51 +12,14 @@ LOG_LEVEL_CODES = {
LOG_FAILURE: 'failure', LOG_FAILURE: 'failure',
} }
# Webhook content types
HTTP_CONTENT_TYPE_JSON = 'application/json' HTTP_CONTENT_TYPE_JSON = 'application/json'
# Models which support registered webhooks # Registerable extras features
WEBHOOK_MODELS = Q( EXTRAS_FEATURES = [
Q(app_label='circuits', model__in=[ 'custom_fields',
'circuit', 'custom_links',
'provider', 'graphs',
]) | 'export_templates',
Q(app_label='dcim', model__in=[ 'webhooks'
'cable', ]
'consoleport',
'consoleserverport',
'device',
'devicebay',
'devicetype',
'frontport',
'interface',
'inventoryitem',
'manufacturer',
'poweroutlet',
'powerpanel',
'powerport',
'powerfeed',
'rack',
'rearport',
'region',
'site',
'virtualchassis',
]) |
Q(app_label='ipam', model__in=[
'aggregate',
'ipaddress',
'prefix',
'service',
'vlan',
'vrf',
]) |
Q(app_label='secrets', model__in=[
'secret',
]) |
Q(app_label='tenancy', model__in=[
'tenant',
]) |
Q(app_label='virtualization', model__in=[
'cluster',
'virtualmachine',
])
)

View File

@ -94,14 +94,14 @@ class GraphFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Graph model = Graph
fields = ['type', 'name', 'template_language'] fields = ['id', 'type', 'name', 'template_language']
class ExportTemplateFilterSet(BaseFilterSet): class ExportTemplateFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ExportTemplate model = ExportTemplate
fields = ['content_type', 'name', 'template_language'] fields = ['id', 'content_type', 'name', 'template_language']
class TagFilterSet(BaseFilterSet): class TagFilterSet(BaseFilterSet):
@ -112,7 +112,7 @@ class TagFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Tag model = Tag
fields = ['name', 'slug'] fields = ['id', 'name', 'slug', 'color']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -219,7 +219,7 @@ class ConfigContextFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ConfigContext model = ConfigContext
fields = ['name', 'is_active'] fields = ['id', 'name', 'is_active']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -255,7 +255,8 @@ class ObjectChangeFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ObjectChange model = ObjectChange
fields = [ fields = [
'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id', 'object_repr', 'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
'object_repr',
] ]
def search(self, queryset, name, value): def search(self, queryset, name, value):

View File

@ -2,13 +2,13 @@ from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from mptt.forms import TreeNodeMultipleChoiceField from mptt.forms import TreeNodeMultipleChoiceField
from taggit.forms import TagField from taggit.forms import TagField as TagField_
from dcim.models import DeviceRole, Platform, Region, Site from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.forms import ( from utilities.forms import (
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
CommentField, ContentTypeSelect, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, ContentTypeSelect, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
) )
from virtualization.models import Cluster, ClusterGroup from virtualization.models import Cluster, ClusterGroup
@ -89,7 +89,7 @@ class CustomFieldModelForm(forms.ModelForm):
return obj return obj
class CustomFieldModelCSVForm(CustomFieldModelForm): class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
def _append_customfield_fields(self): def _append_customfield_fields(self):
@ -142,14 +142,22 @@ class CustomFieldFilterForm(forms.Form):
# Tags # Tags
# #
class TagField(TagField_):
def widget_attrs(self, widget):
# Apply the "tagfield" CSS class to trigger the special API-based selection widget for tags
return {
'class': 'tagfield'
}
class TagForm(BootstrapMixin, forms.ModelForm): class TagForm(BootstrapMixin, forms.ModelForm):
slug = SlugField() slug = SlugField()
comments = CommentField()
class Meta: class Meta:
model = Tag model = Tag
fields = [ fields = [
'name', 'slug', 'color', 'comments' 'name', 'slug', 'color', 'description'
] ]
@ -181,9 +189,13 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
required=False, required=False,
widget=ColorSelect() widget=ColorSelect()
) )
description = forms.CharField(
max_length=200,
required=False
)
class Meta: class Meta:
nullable_fields = [] nullable_fields = ['description']
# #
@ -198,60 +210,35 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
) )
sites = DynamicModelMultipleChoiceField( sites = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False
widget=APISelectMultiple(
api_url="/api/dcim/sites/"
)
) )
roles = DynamicModelMultipleChoiceField( roles = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
required=False, required=False
widget=APISelectMultiple(
api_url="/api/dcim/device-roles/"
)
) )
platforms = DynamicModelMultipleChoiceField( platforms = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
required=False, required=False
widget=APISelectMultiple(
api_url="/api/dcim/platforms/"
)
) )
cluster_groups = DynamicModelMultipleChoiceField( cluster_groups = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
required=False, required=False
widget=APISelectMultiple(
api_url="/api/virtualization/cluster-groups/"
)
) )
clusters = DynamicModelMultipleChoiceField( clusters = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False, required=False
widget=APISelectMultiple(
api_url="/api/virtualization/clusters/"
)
) )
tenant_groups = DynamicModelMultipleChoiceField( tenant_groups = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
required=False, required=False
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/"
)
) )
tenants = DynamicModelMultipleChoiceField( tenants = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/"
)
) )
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
to_field_name='slug', required=False
required=False,
widget=APISelectMultiple(
api_url="/api/extras/tags/"
)
) )
data = JSONField( data = JSONField(
label='' label=''
@ -299,7 +286,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug", value_field="slug",
) )
) )
@ -308,7 +294,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
) )
) )
@ -317,7 +302,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/device-roles/",
value_field="slug", value_field="slug",
) )
) )
@ -326,7 +310,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/platforms/",
value_field="slug", value_field="slug",
) )
) )
@ -335,24 +318,19 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/virtualization/cluster-groups/",
value_field="slug", value_field="slug",
) )
) )
cluster_id = DynamicModelMultipleChoiceField( cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False, required=False,
label='Cluster', label='Cluster'
widget=APISelectMultiple(
api_url="/api/virtualization/clusters/",
)
) )
tenant_group = DynamicModelMultipleChoiceField( tenant_group = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/",
value_field="slug", value_field="slug",
) )
) )
@ -361,7 +339,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug", value_field="slug",
) )
) )
@ -370,7 +347,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/extras/tags/",
value_field="slug", value_field="slug",
) )
) )
@ -456,18 +432,19 @@ class ScriptForm(BootstrapMixin, forms.Form):
def __init__(self, vars, *args, commit_default=True, **kwargs): def __init__(self, vars, *args, commit_default=True, **kwargs):
super().__init__(*args, **kwargs)
# Dynamically populate fields for variables # Dynamically populate fields for variables
for name, var in vars.items(): for name, var in vars.items():
self.fields[name] = var.as_field() self.base_fields[name] = var.as_field()
super().__init__(*args, **kwargs)
# Toggle default commit behavior based on Meta option # Toggle default commit behavior based on Meta option
if not commit_default: if not commit_default:
self.fields['_commit'].initial = False self.fields['_commit'].initial = False
# Move _commit to the end of the form # Move _commit to the end of the form
self.fields.move_to_end('_commit', True) commit = self.fields.pop('_commit')
self.fields['_commit'] = commit
@property @property
def requires_input(self): def requires_input(self):

View File

@ -0,0 +1,16 @@
from django.conf import settings
from django_rq.management.commands.rqworker import Command as _Command
class Command(_Command):
"""
Subclass django_rq's built-in rqworker to listen on all configured queues if none are specified (instead
of only the 'default' queue).
"""
def handle(self, *args, **options):
# If no queues have been specified on the command line, listen on all configured queues.
if len(args) < 1:
args = settings.RQ_QUEUES
super().handle(*args, **options)

View File

@ -0,0 +1,85 @@
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler
from django.core.management.base import BaseCommand
request_counter = 1
class WebhookHandler(BaseHTTPRequestHandler):
show_headers = True
def __getattr__(self, item):
# Return the same method for any type of HTTP request (GET, POST, etc.)
if item.startswith('do_'):
return self.do_ANY
raise AttributeError
def log_message(self, format_str, *args):
global request_counter
print("[{}] {} {} {}".format(
request_counter,
self.date_time_string(),
self.address_string(),
format_str % args
))
def do_ANY(self):
global request_counter
# Send a 200 response regardless of the request content
self.send_response(200)
self.end_headers()
self.wfile.write(b'Webhook received!\n')
request_counter += 1
# Print the request headers to stdout
if self.show_headers:
for k, v in self.headers.items():
print('{}: {}'.format(k, v))
print()
# Print the request body (if any)
content_length = self.headers.get('Content-Length')
if content_length is not None:
body = self.rfile.read(int(content_length))
print(body.decode('utf-8'))
else:
print('(No body)')
print('------------')
class Command(BaseCommand):
help = "Start a simple listener to display received HTTP requests"
default_port = 9000
def add_arguments(self, parser):
parser.add_argument(
'--port', type=int, default=self.default_port,
help="Optional port number (default: {})".format(self.default_port)
)
parser.add_argument(
"--no-headers", action='store_true', dest='no_headers',
help="Hide HTTP request headers"
)
def handle(self, *args, **options):
port = options['port']
quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
WebhookHandler.show_headers = not options['no_headers']
self.stdout.write('Listening on port http://localhost:{}. Stop with {}.'.format(port, quit_command))
httpd = HTTPServer(('localhost', port), WebhookHandler)
try:
httpd.serve_forever()
except KeyboardInterrupt:
self.stdout.write("\nExiting...")

Some files were not shown because too many files have changed in this diff Show More