Merge upstream v2.7.8 into develop

This commit is contained in:
Marco Ceppi 2020-02-27 07:18:06 -05:00
commit e05ab4a16c
1195 changed files with 107886 additions and 16972 deletions

View File

@ -1,4 +1,3 @@
netbox
scripts scripts
.tox .tox
.venv .venv

View File

@ -5,7 +5,9 @@ about: Report a reproducible bug in the current release of NetBox
--- ---
<!-- <!--
NOTE: This form is only for reproducible bugs. If you need assistance with NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
This form is only for reproducible bugs. If you need assistance with
NetBox installation, or if you have a general question, DO NOT open an NetBox installation, or if you have a general question, DO NOT open an
issue. Instead, post to our mailing list: issue. Instead, post to our mailing list:
@ -16,8 +18,8 @@ about: Report a reproducible bug in the current release of NetBox
before submitting a bug report. before submitting a bug report.
--> -->
### Environment ### Environment
* Python version: <!-- Example: 3.5.4 --> * Python version: <!-- Example: 3.6.9 -->
* NetBox version: <!-- Example: 2.5.2 --> * NetBox version: <!-- Example: 2.7.3 -->
<!-- <!--
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

9
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,9 @@
# Reference: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
blank_issues_enabled: false
contact_links:
- name: 📖 Contributing Policy
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
about: Please read through our contributing policy before opening an issue or pull request
- name: 💬 Discussion Group
url: https://groups.google.com/forum/#!forum/netbox-discuss
about: Join our discussion group for assistance with installation issues and other problems

View File

@ -5,6 +5,8 @@ about: Suggest an addition or modification to the NetBox documentation
--- ---
<!-- <!--
NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
Please indicate the nature of the change by placing an X in one of the Please indicate the nature of the change by placing an X in one of the
boxes below. boxes below.
--> -->
@ -14,5 +16,13 @@ about: Suggest an addition or modification to the NetBox documentation
[ ] Deprecation [ ] Deprecation
[ ] Cleanup (formatting, typos, etc.) [ ] Cleanup (formatting, typos, etc.)
### Area
[ ] Installation instructions
[ ] Configuration parameters
[ ] Functionality/features
[ ] REST API
[ ] Administration/development
[ ] Other
<!-- Describe the proposed change(s). --> <!-- Describe the proposed change(s). -->
### Proposed Changes ### Proposed Changes

View File

@ -5,7 +5,9 @@ about: Propose a new NetBox feature or enhancement
--- ---
<!-- <!--
NOTE: This form is only for proposing specific new features or enhancements. NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
This form is only for proposing specific new features or enhancements.
If you have a general idea or question, please post to our mailing list If you have a general idea or question, please post to our mailing list
instead of opening an issue: instead of opening an issue:
@ -19,8 +21,8 @@ about: Propose a new NetBox feature or enhancement
before submitting a bug report. before submitting a bug report.
--> -->
### Environment ### Environment
* Python version: <!-- Example: 3.5.4 --> * Python version: <!-- Example: 3.6.9 -->
* NetBox version: <!-- Example: 2.3.6 --> * NetBox version: <!-- Example: 2.7.3 -->
<!-- <!--
Describe in detail the new functionality you are proposing. Include any Describe in detail the new functionality you are proposing. Include any

View File

@ -1,14 +1,13 @@
--- ---
name: 🏡 Housekeeping name: 🏡 Housekeeping
about: A change pertaining to the codebase itself about: A change pertaining to the codebase itself (developers only)
--- ---
<!-- <!--
NOTE: This type of issue should be opened only by those reasonably familiar NOTE: This template is for use by maintainers only. Please do not submit
with NetBox's code base and interested in contributing to its development. an issue using this template unless you have been specifically asked to
do so.
Describe the proposed change(s) in detail.
--> -->
### Proposed Changes ### Proposed Changes

23
.github/lock.yml vendored Normal file
View File

@ -0,0 +1,23 @@
# Configuration for Lock (https://github.com/apps/lock)
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 90
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: false
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: true
# Limit to only `issues` or `pulls`
# only: issues

10
.github/stale.yml vendored
View File

@ -1,20 +1,30 @@
# Configuration for Stale (https://github.com/apps/stale)
# Pull requests are exempt from being marked as stale
only: issues
# Number of days of inactivity before an issue becomes stale # Number of days of inactivity before an issue becomes stale
daysUntilStale: 14 daysUntilStale: 14
# Number of days of inactivity before a stale issue is closed # Number of days of inactivity before a stale issue is closed
daysUntilClose: 7 daysUntilClose: 7
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- "status: accepted" - "status: accepted"
- "status: gathering feedback" - "status: gathering feedback"
- "status: blocked" - "status: blocked"
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: wontfix staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable
markComment: > markComment: >
This issue has been automatically marked as stale because it has not had This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. NetBox recent activity. It will be closed if no further activity occurs. NetBox
is governed by a small group of core maintainers which means not all opened is governed by a small group of core maintainers which means not all opened
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md). issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
# Comment to post when closing a stale issue. Set to `false` to disable # Comment to post when closing a stale issue. Set to `false` to disable
closeComment: > closeComment: >
This issue has been automatically closed due to lack of activity. In an This issue has been automatically closed due to lack of activity. In an

3
.gitignore vendored
View File

@ -12,6 +12,9 @@
fabfile.py fabfile.py
*.swp *.swp
gunicorn_config.py gunicorn_config.py
gunicorn.py
netbox.log
netbox.pid
.DS_Store .DS_Store
.vscode .vscode
.venv .venv

View File

@ -10,6 +10,7 @@ python:
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install pycodestyle - pip install pycodestyle
- pip install coverage
before_script: before_script:
- psql --version - psql --version
- psql -U postgres -c 'SELECT version();' - psql -U postgres -c 'SELECT version();'

View File

@ -1,4 +1,4 @@
FROM vaporio/python:3.6 FROM vaporio/python:3.7 as builder
RUN apt-get update -qy \ RUN apt-get update -qy \
&& apt-get install -y \ && apt-get install -y \
@ -13,62 +13,49 @@ RUN apt-get update -qy \
ttf-ubuntu-font-family \ ttf-ubuntu-font-family \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN pip install \ WORKDIR /install
RUN pip install --prefix="/install" --no-warn-script-location \
# gunicorn is used for launching netbox # gunicorn is used for launching netbox
gunicorn \ gunicorn \
greenlet \
eventlet \
# napalm is used for gathering information from network devices # napalm is used for gathering information from network devices
napalm \ napalm \
# ruamel is used in startup_scripts # ruamel is used in startup_scripts
ruamel.yaml \ 'ruamel.yaml>=0.15,<0.16' \
# pinning django to the version required by netbox # django-storages was introduced in 2.7 and is optional
# adding it here, to install the correct version of django-storages
# django-rq
'Django>=2.2,<2.3' \
# django-rq is used for webhooks
django-rq
ARG BRANCH ARG NETBOX_PATH=.
ARG ORG=vapor-ware COPY ${NETBOX_PATH}/requirements.txt /
RUN pip install --prefix="/install" --no-warn-script-location -r /requirements.txt
FROM vaporio/python:3.7-slim
# Set image metadata (see: http://label-schema.org/rc1/) RUN apt-get update -qy \
ARG BUILD_VERSION && apt-get install -y \
ARG BUILD_DATE libsasl2-dev \
ARG VCS_REF graphviz \
libjpeg-dev \
LABEL maintainer="Vapor IO"\ libffi-dev \
org.label-schema.schema-version="1.0" \ libxml2-dev \
org.label-schema.build-date=$BUILD_DATE \ libxslt1-dev \
org.label-schema.name="vaporio/netbox" \ libldap2-dev \
org.label-schema.vcs-url="https://github.com/vapor-ware/netbox" \ libpq-dev \
org.label-schema.vcs-ref=$VCS_REF \ ttf-ubuntu-font-family \
org.label-schema.vendor="Vapor IO" \ && rm -rf /var/lib/apt/lists/*
org.label-schema.version=$BUILD_VERSION
WORKDIR /tmp
# As the requirements don't change very often,
# and as they take some time to compile,
# we try to cache them very agressively.
ARG REQUIREMENTS_URL=https://raw.githubusercontent.com/$ORG/netbox/$BRANCH/requirements.txt
ADD ${REQUIREMENTS_URL} requirements.txt
RUN pip install -r requirements.txt
# Cache bust when the upstream branch changes:
# ADD will fetch the file and check if it has changed
# If not, Docker will use the existing build cache.
# If yes, Docker will bust the cache and run every build step from here on.
ARG REF_URL=https://api.github.com/repos/$ORG/netbox/contents?ref=$BRANCH
ADD ${REF_URL} version.json
WORKDIR /opt WORKDIR /opt
ARG URL=https://github.com/$ORG/netbox/archive/$BRANCH.tar.gz COPY --from=builder /install /usr/local
RUN wget -q -O - "${URL}" | tar xz \
&& mv netbox* netbox ARG NETBOX_PATH=.
COPY ${NETBOX_PATH} /opt/netbox
COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
COPY docker/configuration/gunicorn_config.py /etc/netbox/config/ COPY docker/configuration/gunicorn_config.py /etc/netbox/config/
COPY docker/nginx.conf /etc/netbox-nginx/nginx.conf
COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh
COPY docker/startup_scripts/ /opt/netbox/startup_scripts/ COPY docker/startup_scripts/ /opt/netbox/startup_scripts/
COPY docker/initializers/ /opt/netbox/initializers/ COPY docker/initializers/ /opt/netbox/initializers/
@ -76,8 +63,39 @@ COPY docker/configuration/configuration.py /etc/netbox/config/configuration.py
WORKDIR /opt/netbox/netbox WORKDIR /opt/netbox/netbox
RUN mkdir -p static && chmod g+w static media
ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ] ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ]
CMD ["gunicorn", "-c /etc/netbox/config/gunicorn_config.py", "netbox.wsgi"] CMD ["gunicorn", "-c /etc/netbox/config/gunicorn_config.py", "netbox.wsgi"]
LABEL SRC_URL="$URL" ARG BUILD_VERSION
ARG BUILD_DATE
ARG VCS_REF
LABEL maintainer="Vapor IO" \
# See http://label-schema.org/rc1/#build-time-labels
# Also https://microbadger.com/labels
org.label-schema.schema-version="1.0" \
org.label-schema.build-date=$BUILD_DATE \
org.label-schema.name="vaporio/netbox" \
org.label-schema.vcs-url="https://github.com/vapor-ware/netbox" \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vendor="Vapor IO" \
org.label-schema.version=$BUILD_VERSION \
org.label-schema.description="A container based distribution of Vapor IO's Netbox, the free and open IPAM and DCIM solution." \
org.label-schema.url="https://github.com/vapor-ware/netbox" \
org.label-schema.usage="https://github.com/vapor-ware/netbox/wiki" \
org.label-schema.vcs-url="https://github.com/vapor-ware/netbox.git" \
# See https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.title="vaporio/netbox" \
org.opencontainers.image.description="A container based distribution of Vapor IO's Netbox, the free and open IPAM and DCIM solution." \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.authors="Vapor IO." \
org.opencontainers.image.vendor="Vapor IO" \
org.opencontainers.image.url="https://github.com/vapor-ware/netbox" \
org.opencontainers.image.documentation="https://github.com/vapor-ware/netbox/wiki" \
org.opencontainers.image.source="https://github.com/vapor-ware/netbox.git" \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.version=$BUILD_VERSION

View File

@ -26,8 +26,12 @@ or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode
![Screenshot of main page](docs/media/screenshot1.png "Main page") ![Screenshot of main page](docs/media/screenshot1.png "Main page")
---
![Screenshot of rack elevation](docs/media/screenshot2.png "Rack elevation") ![Screenshot of rack elevation](docs/media/screenshot2.png "Rack elevation")
---
![Screenshot of prefix hierarchy](docs/media/screenshot3.png "Prefix hierarchy") ![Screenshot of prefix hierarchy](docs/media/screenshot3.png "Prefix hierarchy")
# Installation # Installation

View File

@ -22,14 +22,18 @@ django-filter
# https://github.com/django-mptt/django-mptt # https://github.com/django-mptt/django-mptt
django-mptt django-mptt
# Django integration for RQ (Reqis queuing) # Context managers for PostgreSQL advisory locks
# https://github.com/rq/django-rq # https://github.com/Xof/django-pglocks
django-rq django-pglocks
# Prometheus metrics library for Django # Prometheus metrics library for Django
# https://github.com/korfuri/django-prometheus # https://github.com/korfuri/django-prometheus
django-prometheus django-prometheus
# Django integration for RQ (Reqis queuing)
# https://github.com/rq/django-rq
django-rq
# Abstraction models for rendering and paginating HTML tables # Abstraction models for rendering and paginating HTML tables
# https://github.com/jieter/django-tables2 # https://github.com/jieter/django-tables2
django-tables2 django-tables2
@ -54,9 +58,9 @@ djangorestframework
# https://github.com/axnsan12/drf-yasg # https://github.com/axnsan12/drf-yasg
drf-yasg[validation] drf-yasg[validation]
# Python interface to the graphviz graph rendering utility # Platform-agnostic template rendering engine
# https://github.com/xflr6/graphviz # https://github.com/pallets/jinja
graphviz 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
@ -82,3 +86,15 @@ py-gfm
# Extensive cryptographic library (fork of pycrypto) # Extensive cryptographic library (fork of pycrypto)
# https://github.com/Legrandin/pycryptodome # https://github.com/Legrandin/pycryptodome
pycryptodome pycryptodome
# YAML rendering library
# https://github.com/yaml/pyyaml
PyYAML
# In-memory key/value store used for caching and queuing
# https://github.com/andymccurdy/redis-py
redis
# SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite
svgwrite

16
contrib/gunicorn.py Normal file
View File

@ -0,0 +1,16 @@
# The IP address (typically localhost) and port that the Netbox WSGI process should listen on
bind = '127.0.0.1:8001'
# Number of gunicorn workers to spawn. This should typically be 2n+1, where
# n is the number of CPU cores present.
workers = 5
# Number of threads per worker process
threads = 3
# Timeout (in seconds) for a request to complete
timeout = 120
# The maximum number of requests a worker can handle before being respawned
max_requests = 5000
max_requests_jitter = 500

22
contrib/netbox-rq.service Normal file
View File

@ -0,0 +1,22 @@
[Unit]
Description=NetBox Request Queue Worker
Documentation=https://netbox.readthedocs.io/en/stable/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/netbox
ExecStart=/usr/bin/python3 /opt/netbox/netbox/manage.py rqworker
Restart=on-failure
RestartSec=30
PrivateTmp=true
[Install]
WantedBy=multi-user.target

22
contrib/netbox.service Normal file
View File

@ -0,0 +1,22 @@
[Unit]
Description=NetBox WSGI Service
Documentation=https://netbox.readthedocs.io/en/stable/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
PIDFile=/var/tmp/netbox.pid
WorkingDirectory=/opt/netbox
ExecStart=/usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi
Restart=on-failure
RestartSec=30
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@ -1,40 +1,70 @@
version: '3' version: '3.4'
services: services:
netbox: &netbox
image: vaporio/netbox:develop
build:
context: .
args:
BUILD_VERSION: 'local-dev'
BUILD_DATE: ''
VCS_REF: 'tip'
depends_on:
- postgres
- redis
- redis-cache
- netbox-worker
env_file: docker/env/netbox.env
#user: '1000:1000'
volumes:
- ./netbox:/opt/netbox/netbox
- ./docker/startup_scripts:/opt/netbox/startup_scripts:z,ro
- ./docker/initializers:/opt/netbox/initializers:z,ro
- ./docker/configuration:/etc/netbox/config:z,ro
- ./reports:/etc/netbox/reports:z,ro
- ./scripts:/etc/netbox/scripts:z,ro
- netbox-nginx-config:/etc/netbox-nginx:z
- netbox-static-files:/opt/netbox/netbox/static:z
- netbox-media-files:/opt/netbox/netbox/media:z
netbox-worker:
<<: *netbox
depends_on:
- redis
entrypoint:
- python3
- /opt/netbox/netbox/manage.py
command:
- rqworker
nginx:
command: nginx -g 'daemon off;' -c /etc/netbox-nginx/nginx.conf
image: nginx
depends_on:
- netbox
ports:
- 8080
volumes:
- netbox-static-files:/opt/netbox/netbox/static:ro
- netbox-nginx-config:/etc/netbox-nginx/:ro
postgres: postgres:
image: "postgres:9.6" image: postgres:11-alpine
environment: environment:
POSTGRES_PASSWORD: "12345" POSTGRES_PASSWORD: "12345"
POSTGRES_DB: netbox POSTGRES_DB: netbox
ports: volumes:
- 5432:5432 - netbox-postgres-data:/var/lib/postgresql/data
redis: redis:
image: "redis" image: redis:5-alpine
nginx:
image: "nginx"
volumes: volumes:
- "static:/opt/netbox/netbox/static" - netbox-redis-data:/data
- "./docker/nginx.conf:/etc/nginx/nginx.conf" redis-cache:
ports: image: redis:5-alpine
- 8000:80
depends_on:
- netbox
netbox:
image: "vaporio/netbox:develop"
environment:
SECRET_KEY: secret
DB_HOST: postgres
DB_USER: postgres
DB_PASSWORD: "12345"
ALLOWED_HOSTS: "*"
REDIS_HOST: redis
depends_on:
- redis
- postgres
volumes:
- "./netbox:/opt/netbox/netbox"
- "static:/opt/netbox/netbox/static"
- "./docker/configuration.docker.py:/opt/netbox/netbox/netbox/configuration.py"
- "./docker/startup_scripts/:/opt/netbox/startup_scripts"
- "./docker/initializers/:/opt/netbox/initializers"
volumes: volumes:
static: netbox-static-files:
driver: local
netbox-nginx-config:
driver: local
netbox-media-files:
driver: local
netbox-postgres-data:
driver: local
netbox-redis-data:
driver: local

View File

@ -37,6 +37,10 @@ DATABASE = {
# PostgreSQL password # PostgreSQL password
'HOST': os.environ.get('DB_HOST', 'localhost'), # Database server 'HOST': os.environ.get('DB_HOST', 'localhost'), # Database server
'PORT': os.environ.get('DB_PORT', ''), # Database port (leave blank for default) 'PORT': os.environ.get('DB_PORT', ''), # Database port (leave blank for default)
'OPTIONS': {'sslmode': os.environ.get('DB_SSLMODE', 'prefer')},
# Database connection SSLMODE
'CONN_MAX_AGE': int(os.environ.get('DB_CONN_MAX_AGE', '300')),
# Database connection persistence
} }
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
@ -47,13 +51,22 @@ 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 = {
'HOST': os.environ.get('REDIS_HOST', 'localhost'), 'webhooks': {
'PORT': int(os.environ.get('REDIS_PORT', 6379)), 'HOST': os.environ.get('REDIS_HOST', 'localhost'),
'PASSWORD': os.environ.get('REDIS_PASSWORD', read_secret('redis_password')), 'PORT': int(os.environ.get('REDIS_PORT', 6379)),
'DATABASE': os.environ.get('REDIS_DATABASE', '0'), 'PASSWORD': os.environ.get('REDIS_PASSWORD', read_secret('redis_password')),
'CACHE_DATABASE': os.environ.get('REDIS_CACHE_DATABASE', '1'), 'DATABASE': int(os.environ.get('REDIS_DATABASE', 0)),
'DEFAULT_TIMEOUT': os.environ.get('REDIS_TIMEOUT', '300'), 'DEFAULT_TIMEOUT': int(os.environ.get('REDIS_TIMEOUT', 300)),
'SSL': os.environ.get('REDIS_SSL', 'False').lower() == 'true', 'SSL': os.environ.get('REDIS_SSL', 'False').lower() == 'true',
},
'caching': {
'HOST': os.environ.get('REDIS_CACHE_HOST', os.environ.get('REDIS_HOST', 'localhost')),
'PORT': int(os.environ.get('REDIS_CACHE_PORT', os.environ.get('REDIS_PORT', 6379))),
'PASSWORD': os.environ.get('REDIS_CACHE_PASSWORD', os.environ.get('REDIS_PASSWORD', read_secret('redis_cache_password'))),
'DATABASE': int(os.environ.get('REDIS_CACHE_DATABASE', 1)),
'DEFAULT_TIMEOUT': int(os.environ.get('REDIS_CACHE_TIMEOUT', os.environ.get('REDIS_TIMEOUT', 300))),
'SSL': os.environ.get('REDIS_CACHE_SSL', os.environ.get('REDIS_SSL', 'False')).lower() == 'true',
},
} }
######################### #########################
@ -161,13 +174,13 @@ PREFER_IPV4 = os.environ.get('PREFER_IPV4', 'False').lower() == 'true'
# 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')
# The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of
# this setting is derived from the installed location.
SCRIPTS_ROOT = os.environ.get('SCRIPTS_ROOT', '/etc/netbox/scripts')
# Time zone (default: UTC) # Time zone (default: UTC)
TIME_ZONE = os.environ.get('TIME_ZONE', 'UTC') TIME_ZONE = os.environ.get('TIME_ZONE', 'UTC')
# The Webhook event backend is disabled by default. Set this to True to enable it. Note that this requires a Redis
# database be configured and accessible by NetBox (see `REDIS` below).
WEBHOOKS_ENABLED = os.environ.get('WEBHOOKS_ENABLED', 'False').lower() == 'true'
# Date/time formatting. See the following link for supported formats: # Date/time formatting. See the following link for supported formats:
# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
DATE_FORMAT = os.environ.get('DATE_FORMAT', 'N j, Y') DATE_FORMAT = os.environ.get('DATE_FORMAT', 'N j, Y')

View File

@ -70,8 +70,7 @@ AUTH_LDAP_USER_FLAGS_BY_GROUP = {
AUTH_LDAP_FIND_GROUP_PERMS = os.environ.get('AUTH_LDAP_FIND_GROUP_PERMS', 'True').lower() == 'true' AUTH_LDAP_FIND_GROUP_PERMS = os.environ.get('AUTH_LDAP_FIND_GROUP_PERMS', 'True').lower() == 'true'
# Cache groups for one hour to reduce LDAP traffic # Cache groups for one hour to reduce LDAP traffic
AUTH_LDAP_CACHE_GROUPS = os.environ.get('AUTH_LDAP_CACHE_GROUPS', 'True').lower() == 'true' AUTH_LDAP_CACHE_TIMEOUT = int(os.environ.get('AUTH_LDAP_CACHE_TIMEOUT', 3600))
AUTH_LDAP_GROUP_CACHE_TIMEOUT = int(os.environ.get('AUTH_LDAP_GROUP_CACHE_TIMEOUT', 3600))
# Populate the Django user from the LDAP directory. # Populate the Django user from the LDAP directory.
AUTH_LDAP_USER_ATTR_MAP = { AUTH_LDAP_USER_ATTR_MAP = {

30
docker/env/netbox.env vendored Normal file
View File

@ -0,0 +1,30 @@
CORS_ORIGIN_ALLOW_ALL=True
DB_NAME=netbox
DB_USER=postgres
DB_PASSWORD=12345
DB_HOST=postgres
EMAIL_SERVER=localhost
EMAIL_PORT=25
EMAIL_USERNAME=netbox
EMAIL_PASSWORD=
EMAIL_TIMEOUT=5
EMAIL_FROM=netbox@bar.com
MEDIA_ROOT=/opt/netbox/netbox/media
NAPALM_USERNAME=
NAPALM_PASSWORD=
NAPALM_TIMEOUT=10
MAX_PAGE_SIZE=1000
REDIS_HOST=redis
REDIS_DATABASE=0
REDIS_SSL=false
REDIS_CACHE_HOST=redis-cache
REDIS_CACHE_DATABASE=0
REDIS_CACHE_SSL=false
SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
SKIP_STARTUP_SCRIPTS=false
SKIP_SUPERUSER=false
SUPERUSER_NAME=admin
SUPERUSER_EMAIL=admin@example.com
SUPERUSER_PASSWORD=admin
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
WEBHOOKS_ENABLED=true

View File

@ -1,3 +1,4 @@
daemon off;
worker_processes 1; worker_processes 1;
error_log /dev/stderr info; error_log /dev/stderr info;
@ -17,7 +18,7 @@ http {
client_max_body_size 10M; client_max_body_size 10M;
server { server {
listen 80; listen 8080;
access_log off; access_log off;
location /static/ { location /static/ {

View File

@ -1,19 +1,9 @@
from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_URL, CF_TYPE_SELECT, CF_FILTER_CHOICES
from extras.models import CustomField, CustomFieldChoice from extras.models import CustomField, CustomFieldChoice
from ruamel.yaml import YAML from ruamel.yaml import YAML
from pathlib import Path from pathlib import Path
import sys import sys
text_to_fields = {
'boolean': CF_TYPE_BOOLEAN,
'date': CF_TYPE_DATE,
'integer': CF_TYPE_INTEGER,
'selection': CF_TYPE_SELECT,
'text': CF_TYPE_TEXT,
'url': CF_TYPE_URL,
}
def get_class_for_class_path(class_path): def get_class_for_class_path(class_path):
import importlib import importlib
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -42,12 +32,6 @@ with file.open('r') as stream:
if cf_details.get('description', 0): if cf_details.get('description', 0):
custom_field.description = cf_details['description'] custom_field.description = cf_details['description']
# If no filter_logic is specified then it will default to 'Loose'
if cf_details.get('filter_logic', 0):
for choice_id, choice_text in CF_FILTER_CHOICES:
if choice_text.lower() == cf_details['filter_logic']:
custom_field.filter_logic = choice_id
if cf_details.get('label', 0): if cf_details.get('label', 0):
custom_field.label = cf_details['label'] custom_field.label = cf_details['label']
@ -58,7 +42,7 @@ with file.open('r') as stream:
custom_field.required = cf_details['required'] custom_field.required = cf_details['required']
if cf_details.get('type', 0): if cf_details.get('type', 0):
custom_field.type = text_to_fields[cf_details['type']] custom_field.type = cf_details['type']
if cf_details.get('weight', 0): if cf_details.get('weight', 0):
custom_field.weight = cf_details['weight'] custom_field.weight = cf_details['weight']

View File

@ -1,7 +1,6 @@
from dcim.models import Site, RackRole, Rack, RackGroup from dcim.models import Site, RackRole, Rack, RackGroup
from tenancy.models import Tenant from tenancy.models import Tenant
from extras.models import CustomField, CustomFieldValue from extras.models import CustomField, CustomFieldValue
from dcim.constants import RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES
from ruamel.yaml import YAML from ruamel.yaml import YAML
from pathlib import Path from pathlib import Path
import sys import sys
@ -41,14 +40,6 @@ with file.open('r') as stream:
params[assoc] = model.objects.get(**query) params[assoc] = model.objects.get(**query)
for rack_type in RACK_TYPE_CHOICES:
if params['type'] in rack_type:
params['type'] = rack_type[0]
for rack_width in RACK_WIDTH_CHOICES:
if params['width'] in rack_width:
params['width'] = rack_width[0]
rack, created = Rack.objects.get_or_create(**params) rack, created = Rack.objects.get_or_create(**params)
if created: if created:

View File

@ -1,5 +1,4 @@
from dcim.models import Site, Rack, DeviceRole, DeviceType, Device, Platform from dcim.models import Site, Rack, DeviceRole, DeviceType, Device, Platform
from dcim.constants import RACK_FACE_CHOICES
from ipam.models import IPAddress from ipam.models import IPAddress
from virtualization.models import Cluster from virtualization.models import Cluster
from tenancy.models import Tenant from tenancy.models import Tenant
@ -26,6 +25,7 @@ with file.open('r') as stream:
optional_assocs = { optional_assocs = {
'tenant': (Tenant, 'name'), 'tenant': (Tenant, 'name'),
'platform': (Platform, 'name'), 'platform': (Platform, 'name'),
'rack': (Rack, 'name'),
'cluster': (Cluster, 'name'), 'cluster': (Cluster, 'name'),
'primary_ip4': (IPAddress, 'address'), 'primary_ip4': (IPAddress, 'address'),
'primary_ip6': (IPAddress, 'address') 'primary_ip6': (IPAddress, 'address')
@ -48,14 +48,6 @@ with file.open('r') as stream:
params[assoc] = model.objects.get(**query) params[assoc] = model.objects.get(**query)
if 'rack' in params:
params['rack'] = Rack.objects.get(name=params.pop('rack'), site=params['site'].id)
if 'face' in params:
for rack_face in RACK_FACE_CHOICES:
if params['face'] in rack_face:
params['face'] = rack_face[0]
device, created = Device.objects.get_or_create(**params) device, created = Device.objects.get_or_create(**params)
if created: if created:

View File

@ -1,5 +1,4 @@
from dcim.models import Interface, Device, DeviceRole from dcim.models import Interface, Device, DeviceRole
from dcim.constants import IFACE_TYPE_KEYSTONE
from ruamel.yaml import YAML from ruamel.yaml import YAML
from pathlib import Path from pathlib import Path
@ -31,6 +30,6 @@ with file.open('r') as stream:
i += 2 i += 2
interface, created = Interface.objects.get_or_create(name=name, device=locker, type=IFACE_TYPE_KEYSTONE) interface, created = Interface.objects.get_or_create(name=name, device=locker, type='keystone')
if created: if created:
print("🔗 Created interface {} for {}".format(interface.name, locker.name)) print("🔗 Created interface {} for {}".format(interface.name, locker.name))

View File

@ -0,0 +1,51 @@
from dcim.models import Site
from ipam.models import VLAN, VLANGroup, Role
from tenancy.models import Tenant, TenantGroup
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/vlans.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
vlans = yaml.load(stream)
optional_assocs = {
'site': (Site, 'name'),
'tenant': (Tenant, 'name'),
'tenant_group': (TenantGroup, 'name'),
'group': (VLANGroup, 'name'),
'role': (Role, 'name')
}
if vlans is not None:
for params in vlans:
custom_fields = params.pop('custom_fields', None)
for assoc, details in optional_assocs.items():
if assoc in params:
model, field = details
query = { field: params.pop(assoc) }
params[assoc] = model.objects.get(**query)
vlan, created = VLAN.objects.get_or_create(**params)
if created:
if custom_fields is not None:
for cf_name, cf_value in custom_fields.items():
custom_field = CustomField.objects.get(name=cf_name)
custom_field_value = CustomFieldValue.objects.create(
field=custom_field,
obj=vlan,
value=cf_value
)
vlan.custom_field_values.add(custom_field_value)
print("🏠 Created VLAN", vlan.name)

View File

@ -0,0 +1,54 @@
from dcim.models import Site
from ipam.models import Prefix, VLAN, Role, VRF
from tenancy.models import Tenant, TenantGroup
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from netaddr import IPNetwork
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/prefixes.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
prefixes = yaml.load(stream)
optional_assocs = {
'site': (Site, 'name'),
'tenant': (Tenant, 'name'),
'tenant_group': (TenantGroup, 'name'),
'vlan': (VLAN, 'name'),
'role': (Role, 'name'),
'vrf': (VRF, 'name')
}
if prefixes is not None:
for params in prefixes:
custom_fields = params.pop('custom_fields', None)
params['prefix'] = IPNetwork(params['prefix'])
for assoc, details in optional_assocs.items():
if assoc in params:
model, field = details
query = { field: params.pop(assoc) }
params[assoc] = model.objects.get(**query)
prefix, created = Prefix.objects.get_or_create(**params)
if created:
if custom_fields is not None:
for cf_name, cf_value in custom_fields.items():
custom_field = CustomField.objects.get(name=cf_name)
custom_field_value = CustomFieldValue.objects.create(
field=custom_field,
obj=prefix,
value=cf_value
)
prefix.custom_field_values.add(custom_field_value)
print("📌 Created Prefix", prefix.prefix)

View File

@ -0,0 +1,44 @@
from dcim.models import Interface, Device
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/dcim_interfaces.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
interfaces = yaml.load(stream)
required_assocs = {
'device': (Device, 'name')
}
if interfaces is not None:
for params in interfaces:
custom_fields = params.pop('custom_fields', None)
for assoc, details in required_assocs.items():
model, field = details
query = { field: params.pop(assoc) }
params[assoc] = model.objects.get(**query)
interface, created = Interface.objects.get_or_create(**params)
if created:
if custom_fields is not None:
for cf_name, cf_value in custom_fields.items():
custom_field = CustomField.objects.get(name=cf_name)
custom_field_value = CustomFieldValue.objects.create(
field=custom_field,
obj=interface,
value=cf_value
)
interface.custom_field_values.add(custom_field_value)
print("🧷 Created interface", interface.name, interface.device.name)

View File

@ -0,0 +1,65 @@
from ipam.models import IPAddress, VRF
from dcim.models import Device, Interface
from virtualization.models import VirtualMachine
from tenancy.models import Tenant
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from netaddr import IPNetwork
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/ip_addresses.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
ip_addresses = yaml.load(stream)
optional_assocs = {
'tenant': (Tenant, 'name'),
'vrf': (VRF, 'name'),
'interface': (Interface, 'name')
}
if ip_addresses is not None:
for params in ip_addresses:
vm = params.pop('virtual_machine', None)
device = params.pop('device', None)
custom_fields = params.pop('custom_fields', None)
params['address'] = IPNetwork(params['address'])
if vm and device:
print("IP Address can only specify one of the following: virtual_machine or device.")
sys.exit()
for assoc, details in optional_assocs.items():
if assoc in params:
model, field = details
if assoc == 'interface':
if vm:
vm_id = VirtualMachine.objects.get(name=vm).id
query = { field: params.pop(assoc), "virtual_machine_id": vm_id }
elif device:
dev_id = Device.objects.get(name=device).id
query = { field: params.pop(assoc), "device_id": dev_id }
else:
query = { field: params.pop(assoc) }
params[assoc] = model.objects.get(**query)
ip_address, created = IPAddress.objects.get_or_create(**params)
if created:
if custom_fields is not None:
for cf_name, cf_value in custom_fields.items():
custom_field = CustomField.objects.get(name=cf_name)
custom_field_value = CustomFieldValue.objects.create(
field=custom_field,
obj=ip_address,
value=cf_value
)
ip_address.custom_field_values.add(custom_field_value)
print("🧬 Created IP Address", ip_address.address)

View File

@ -27,11 +27,17 @@ class MyScript(Script):
var2 = IntegerVar(...) var2 = IntegerVar(...)
var3 = ObjectVar(...) var3 = ObjectVar(...)
def run(self, data): def run(self, data, commit):
... ...
``` ```
The `run()` method is passed a single argument: a dictionary containing all of the variable data passed via the web form. Your script can reference this data during execution. The `run()` method should accept two arguments:
* `data` - A dictionary containing all of the variable data passed via the web form.
* `commit` - A boolean indicating whether database changes will be committed.
!!! note
The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however moving forward scripts should accept both arguments.
Defining variables is optional: You may create a script with only a `run()` method if no user input is needed. Defining variables is optional: You may create a script with only a `run()` method if no user input is needed.
@ -124,7 +130,7 @@ Arbitrary text of any length. Renders as multi-line text input field.
Stored a numeric integer. Options include: Stored a numeric integer. Options include:
* `min_value:` - Minimum value * `min_value` - Minimum value
* `max_value` - Maximum value * `max_value` - Maximum value
### BooleanVar ### BooleanVar
@ -158,18 +164,30 @@ A NetBox object. The list of available objects is defined by the queryset parame
An uploaded file. Note that uploaded files are present in memory only for the duration of the script's execution: They will not be save for future use. An uploaded file. Note that uploaded files are present in memory only for the duration of the script's execution: They will not be save for future use.
### IPAddressVar
An IPv4 or IPv6 address, without a mask. Returns a `netaddr.IPAddress` object.
### IPAddressWithMaskVar
An IPv4 or IPv6 address with a mask. Returns a `netaddr.IPNetwork` object which includes the mask.
### IPNetworkVar ### IPNetworkVar
An IPv4 or IPv6 network with a mask. An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two attributes are available to validate the provided mask:
* `min_prefix_length` - Minimum length of the mask (default: none)
* `max_prefix_length` - Maximum length of the mask (default: none)
### Default Options ### Default Options
All variables support the following default options: All variables support the following default options:
* `label` - The name of the form field
* `description` - A brief description of the field
* `default` - The field's default value * `default` - The field's default value
* `description` - A brief description of the field
* `label` - The name of the form field
* `required` - Indicates whether the field is mandatory (default: true) * `required` - Indicates whether the field is mandatory (default: true)
* `widget` - The class of form widget to use (see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/forms/widgets/))
## Example ## Example
@ -184,7 +202,7 @@ These variables are presented as a web form to be completed by the user. Once su
``` ```
from django.utils.text import slugify from django.utils.text import slugify
from dcim.constants import * from dcim.choices import DeviceStatusChoices, SiteStatusChoices
from dcim.models import Device, DeviceRole, DeviceType, Site from dcim.models import Device, DeviceRole, DeviceType, Site
from extras.scripts import * from extras.scripts import *
@ -210,13 +228,13 @@ class NewBranchScript(Script):
) )
) )
def run(self, data): def run(self, data, commit):
# Create the new site # Create the new site
site = Site( site = Site(
name=data['site_name'], name=data['site_name'],
slug=slugify(data['site_name']), slug=slugify(data['site_name']),
status=SITE_STATUS_PLANNED status=SiteStatusChoices.STATUS_PLANNED
) )
site.save() site.save()
self.log_success("Created new site: {}".format(site)) self.log_success("Created new site: {}".format(site))
@ -228,7 +246,7 @@ class NewBranchScript(Script):
device_type=data['switch_model'], device_type=data['switch_model'],
name='{}-switch{}'.format(site.slug, i), name='{}-switch{}'.format(site.slug, i),
site=site, site=site,
status=DEVICE_STATUS_PLANNED, status=DeviceStatusChoices.STATUS_PLANNED,
device_role=switch_role device_role=switch_role
) )
switch.save() switch.save()

View File

@ -8,6 +8,11 @@ NetBox does not have the ability to generate graphs natively, but this feature a
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`. * **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`. * **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
Graph names and links can be rendered using the Django or Jinja2 template languages.
!!! warning
Support for the Django templating language will be removed in NetBox v2.8. Jinja2 is recommended.
## Examples ## Examples
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this: You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:

View File

@ -0,0 +1,65 @@
# NAPALM
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API.
!!! info
To enable the integration, the NAPALM library must be installed. See [installation steps](../../installation/2-netbox/#napalm-automation-optional) for more information.
```
GET /api/dcim/devices/1/napalm/?method=get_environment
{
"get_environment": {
...
}
}
```
## Authentication
By default, the [`NAPALM_USERNAME`](../../configuration/optional-settings/#napalm_username) and [`NAPALM_PASSWORD`](../../configuration/optional-settings/#napalm_password) are used for NAPALM authentication. They can be overridden for an individual API call through the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
```
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
-H "Authorization: Token f4b378553dacfcfd44c5a0b9ae49b57e29c552b5" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
-H "X-NAPALM-Username: foo" \
-H "X-NAPALM-Password: bar"
```
## Method Support
The list of supported NAPALM methods depends on the [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html#general-support-matrix) configured for the platform of a device. NetBox only supports [get](https://napalm.readthedocs.io/en/latest/support/index.html#getters-support-matrix) methods.
## Multiple Methods
More than one method in an API call can be invoked by adding multiple `method` parameters. For example:
```
GET /api/dcim/devices/1/napalm/?method=get_ntp_servers&method=get_ntp_peers
{
"get_ntp_servers": {
...
},
"get_ntp_peers": {
...
}
}
```
## Optional Arguments
The behavior of NAPALM drivers can be adjusted according to the [optional arguments](https://napalm.readthedocs.io/en/latest/support/index.html#optional-arguments). NetBox exposes those arguments using headers prefixed with `X-NAPALM-`.
For instance, the SSH port is changed to 2222 in this API call:
```
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
-H "Authorization: Token f4b378553dacfcfd44c5a0b9ae49b57e29c552b5" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
-H "X-NAPALM-port: 2222"
```

View File

@ -32,7 +32,8 @@ class DeviceIPsReport(Report):
Within each report class, we'll create a number of test methods to execute our report's logic. In DeviceConnectionsReport, for instance, we want to ensure that every live device has a console connection, an out-of-band management connection, and two power connections. Within each report class, we'll create a number of test methods to execute our report's logic. In DeviceConnectionsReport, for instance, we want to ensure that every live device has a console connection, an out-of-band management connection, and two power connections.
``` ```
from dcim.constants import CONNECTION_STATUS_PLANNED, DEVICE_STATUS_ACTIVE 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
@ -43,7 +44,8 @@ class DeviceConnectionsReport(Report):
def test_console_connection(self): def test_console_connection(self):
# Check that every console port for every active device has a connection defined. # Check that every console port for every active device has a connection defined.
for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=DEVICE_STATUS_ACTIVE): active = DeviceStatusChoices.STATUS_ACTIVE
for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=active):
if console_port.connected_endpoint is None: if console_port.connected_endpoint is None:
self.log_failure( self.log_failure(
console_port.device, console_port.device,
@ -60,7 +62,7 @@ class DeviceConnectionsReport(Report):
def test_power_connections(self): def test_power_connections(self):
# Check that every active device has at least two connected power supplies. # Check that every active device has at least two connected power supplies.
for device in Device.objects.filter(status=DEVICE_STATUS_ACTIVE): for device in Device.objects.filter(status=DeviceStatusChoices.STATUS_ACTIVE):
connected_ports = 0 connected_ports = 0
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:

View File

@ -1,17 +0,0 @@
# Topology Maps
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure).
To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
Each line of the **device patterns** field represents a hierarchical layer within the topology map. For example, you might map a traditional network with core, distribution, and access tiers like this:
```
core-switch-[abcd]
dist-switch\d
access-switch\d+;oob-switch\d+
```
Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.

View File

@ -1,61 +1,73 @@
# Webhooks # Webhooks
A webhook defines an HTTP request that is sent to an external application when certain types of objects are created, updated, and/or deleted in NetBox. When a webhook is triggered, a POST request is sent to its configured URL. This request will include a full representation of the object being modified for consumption by the receiver. Webhooks are configured via the admin UI under Extras > Webhooks. A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever a device status is changed in NetBox. This can be done by creating a webhook for the device model in NetBox. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are configured in the admin UI under Extras > Webhooks.
An optional secret key can be configured for each webhook. This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. This digest can be used by the receiver to authenticate the request's content. ## Configuration
## Requests * **Name** - A unique name for the webhook. The name is not included with outbound messages.
* **Object type(s)** - The type or types of NetBox object that will trigger the webhook.
* **Enabled** - If unchecked, the webhook will be inactive.
* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected.
* **HTTP method** - The type of HTTP request to send. Options include GET, POST, PUT, PATCH, and DELETE.
* **URL** - The fuly-qualified URL of the request to be sent. This may specify a destination port number if needed.
* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`)
* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below).
* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.)
* **Secret** - A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key.
* **SSL verification** - Uncheck this option to disable validation of the receiver's SSL certificate. (Disable with caution!)
* **CA file path** - The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (optional).
The webhook POST request is structured as so (assuming `application/json` as the Content-Type): ## Jinja2 Template Support
[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `additional_headers` and `body_template` fields. This enables the user to convey change data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands.
For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration:
* Object type: IPAM > IP address
* HTTP method: POST
* URL: <Slack incoming webhook URL>
* HTTP content type: `application/json`
* Body template: `{"text": "IP address {{ data['address'] }} was created by {{ username }}!"}`
### Available Context
The following data is available as context for Jinja2 templates:
* `event` - The type of event which triggered the webhook: created, updated, or deleted.
* `model` - The NetBox model which triggered the change.
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
* `username` - The name of the user account associated with the change.
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
* `data` - A serialized representation of the object _after_ the change was made. This is typically equivalent to the model's representation in NetBox's REST API.
### Default Request Body
If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows:
```no-highlight ```no-highlight
{ {
"event": "created", "event": "created",
"timestamp": "2019-10-12 12:51:29.746944", "timestamp": "2020-02-25 15:10:26.010582+00:00",
"username": "admin",
"model": "site", "model": "site",
"request_id": "43d8e212-94c7-4f67-b544-0dcde4fc0f43", "username": "jstretch",
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
"data": { "data": {
"id": 19,
"name": "Site 1",
"slug": "site-1",
"status":
"value": "active",
"label": "Active",
"id": 1
},
"region": null,
... ...
} }
} }
``` ```
`data` is the serialized representation of the model instance(s) from the event. The same serializers from the NetBox API are used. So an example of the payload for a Site delete event would be: ## Webhook Processing
```no-highlight 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.
{
"event": "deleted",
"timestamp": "2019-10-12 12:55:44.030750",
"username": "johnsmith",
"model": "site",
"request_id": "e9bb83b2-ebe4-4346-b13f-07144b1a00b4",
"data": {
"asn": None,
"comments": "",
"contact_email": "",
"contact_name": "",
"contact_phone": "",
"count_circuits": 0,
"count_devices": 0,
"count_prefixes": 0,
"count_racks": 0,
"count_vlans": 0,
"custom_fields": {},
"facility": "",
"id": 54,
"name": "test",
"physical_address": "",
"region": None,
"shipping_address": "",
"slug": "test",
"tenant": None
}
}
```
A request is considered successful if the response status code is any one of a list of "good" statuses defined in the [requests library](https://github.com/requests/requests/blob/205755834d34a8a6ecf2b0b5b2e9c3e6a7f4e4b6/requests/models.py#L688), otherwise the request is marked as having failed. The user may manually retry a failed request. 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.
## Backend Status
Django-rq includes a status page in the admin site which can be used to view the result of processed webhooks and manually retry any failed webhooks. Access it from http://netbox.local/admin/webhook-backend-status/.

View File

@ -90,6 +90,14 @@ This setting enables debugging. This should be done only during development or t
--- ---
## DEVELOPER
Default: False
This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Set this to `True` **only** if you are actively developing the NetBox code base.
---
## 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` setting:
@ -101,6 +109,20 @@ In order to send email, NetBox needs an email server configured. The following i
* TIMEOUT - Amount of time to wait for a connection (seconds) * TIMEOUT - Amount of time to wait for a connection (seconds)
* FROM_EMAIL - Sender address for emails sent by NetBox * FROM_EMAIL - Sender address for emails sent by NetBox
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):
```
# python ./manage.py nbshell
>>> from django.core.mail import send_mail
>>> send_mail(
'Test Email Subject',
'Test Email Body',
'noreply-netbox@example.com',
['users@example.com'],
fail_silently=False
)
```
--- ---
## EXEMPT_VIEW_PERMISSIONS ## EXEMPT_VIEW_PERMISSIONS
@ -127,7 +149,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
--- ---
# ENFORCE_GLOBAL_UNIQUE ## ENFORCE_GLOBAL_UNIQUE
Default: False Default: False
@ -293,6 +315,26 @@ Session data is used to track authenticated users when they access NetBox. By de
--- ---
## STORAGE_BACKEND
Default: None (local storage)
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) package, which provides backends for several popular file storage services. If not configured, local filesystem storage will be used.
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
---
## STORAGE_CONFIG
Default: Empty
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the [`django-storages` documentation](https://django-storages.readthedocs.io/en/stable/) for more detail.
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
---
## TIME_ZONE ## TIME_ZONE
Default: UTC Default: UTC
@ -301,14 +343,6 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
--- ---
## WEBHOOKS_ENABLED
Default: False
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../../additional-features/webhooks/) for more information on setup and use.
---
## Date and Time Formatting ## Date and Time Formatting
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date). You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date).

View File

@ -21,11 +21,11 @@ NetBox requires access to a PostgreSQL database service to store data. This serv
* `PASSWORD` - PostgreSQL password * `PASSWORD` - PostgreSQL password
* `HOST` - Name or IP address of the database server (use `localhost` if running locally) * `HOST` - Name or IP address of the database server (use `localhost` if running locally)
* `PORT` - TCP port of the PostgreSQL service; leave blank for default port (5432) * `PORT` - TCP port of the PostgreSQL service; leave blank for default port (5432)
* `CONN_MAX_AGE` - Number in seconds for Netbox to keep database connections open. 150-300 seconds is typically a good starting point ([more info](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections)). * `CONN_MAX_AGE` - Lifetime of a [persistent database connection](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections), in seconds (150-300 is recommended)
Example: Example:
``` ```python
DATABASE = { DATABASE = {
'NAME': 'netbox', # Database name 'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username 'USER': 'netbox', # PostgreSQL username
@ -36,46 +36,99 @@ DATABASE = {
} }
``` ```
!!! note
NetBox supports all PostgreSQL database options supported by the underlying Django framework. For a complete list of available parameters, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#databases).
--- ---
## REDIS ## REDIS
[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). 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.
Redis is configured using a configuration setting similar to `DATABASE`: Redis is configured using a configuration setting similar to `DATABASE` and these settings are the same for both of the `webhooks` 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)
* `PASSWORD` - Redis password (if set) * `PASSWORD` - Redis password (if set)
* `DATABASE` - Numeric database ID for webhooks * `DATABASE` - Numeric database ID
* `CACHE_DATABASE` - Numeric database ID for caching
* `DEFAULT_TIMEOUT` - Connection timeout in seconds * `DEFAULT_TIMEOUT` - Connection timeout in seconds
* `SSL` - Use SSL connection to Redis * `SSL` - Use SSL connection to Redis
Example: Example:
``` ```python
REDIS = { REDIS = {
'HOST': 'localhost', 'webhooks': {
'PORT': 6379, 'HOST': 'redis.example.com',
'PASSWORD': '', 'PORT': 1234,
'DATABASE': 0, 'PASSWORD': 'foobar',
'CACHE_DATABASE': 1, 'DATABASE': 0,
'DEFAULT_TIMEOUT': 300, 'DEFAULT_TIMEOUT': 300,
'SSL': False, 'SSL': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
} }
``` ```
!!! note: !!! note
If you were using these settings in a prior release with webhooks, the `DATABASE` setting remains the same but If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have
an additional `CACHE_DATABASE` setting has been added with a default value of 1 to support the caching backend. The changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary
`DATABASE` setting will be renamed in a future release of NetBox to better relay the meaning of the setting.
!!! note
It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the
same Redis instance for both may result in webhook processing data being lost during cache flushing events.
### Using Redis Sentinel
If you are using [Redis Sentinel](https://redis.io/topics/sentinel) for high-availability purposes, there is minimal
configuration necessary to convert NetBox to recognize it. It requires the removal of the `HOST` and `PORT` keys from
above and the addition of two new keys.
* `SENTINELS`: List of tuples or tuple of tuples with each inner tuple containing the name or IP address
of the Redis server and port for each sentinel instance to connect to
* `SENTINEL_SERVICE`: Name of the master / service to connect to
Example:
```python
REDIS = {
'webhooks': {
'SENTINELS': [('mysentinel.redis.example.com', 6379)],
'SENTINEL_SERVICE': 'netbox',
'PASSWORD': '',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
},
'caching': {
'SENTINELS': [
('mysentinel.redis.example.com', 6379),
('othersentinel.redis.example.com', 6379)
],
'SENTINEL_SERVICE': 'netbox',
'PASSWORD': '',
'DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
}
```
!!! note
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
`SENTINELS`/`SENTINEL_SERVICE`.
!!! warning:
It is highly recommended to keep the webhook and cache databases seperate. Using the same database number for both may result in webhook
processing data being lost in cache flushing events.
--- ---

View File

@ -0,0 +1,58 @@
# Power Panel
A power panel represents the distribution board where power circuits and their circuit breakers terminate on. If you have multiple power panels in your data center, you should model them as such in NetBox to assist you in determining the redundancy of your power allocation.
# Power Feed
A power feed identifies the power outlet/drop that goes to a rack and is terminated to a power panel. Power feeds have a supply type (AC/DC), voltage, amperage, and phase type (single/three).
Power feeds are optionally assigned to a rack. In addition, a power port and only one can connect to a power feed; in the context of a PDU, the power feed is analogous to the power outlet that a PDU's power port/inlet connects to.
!!! info
The power usage of a rack is calculated when a power feed (or multiple) is assigned to that rack and connected to a power port.
# Power Outlet
Power outlets represent the ports on a PDU that supply power to other devices. Power outlets are downstream-facing towards power ports. A power outlet can be associated with a power port on the same device and a feed leg (i.e. in a case of a three-phase supply). This indicates which power port supplies power to a power outlet.
# Power Port
A power port is the inlet of a device where it draws its power. Power ports are upstream-facing towards power outlets. Alternatively, a power port can connect to a power feed as mentioned in the power feed section to indicate the power source of a PDU's inlet.
!!! info
If the draw of a power port is left empty, it will be dynamically calculated based on the power outlets associated with that power port. This is usually the case on the power ports of devices that supply power, like a PDU.
# Example
Below is a simple diagram demonstrating how power is modelled in NetBox.
!!! note
The power feeds are connected to the same power panel for illustrative purposes; usually, you would have such feeds diversely connected to panels to avoid the single point of failure.
```
+---------------+
| Power panel 1 |
+---------------+
| |
| |
+--------------+ +--------------+
| Power feed 1 | | Power feed 2 |
+--------------+ +--------------+
| |
| |
| | <-- Power ports
+---------+ +---------+
| PDU 1 | | PDU 2 |
+---------+ +---------+
| \ / | <-- Power outlets
| \ / |
| \ / |
| X |
| / \ |
| / \ |
| / \ | <-- Power ports
+--------+ +--------+
| Server | | Router |
+--------+ +--------+
```

View File

@ -24,6 +24,20 @@ Each user within NetBox can associate his or her account with an RSA public key.
User keys may be created by users individually, however they are of no use until they have been activated by a user who already possesses an active user key. User keys may be created by users individually, however they are of no use until they have been activated by a user who already possesses an active user key.
## Supported Key Format
Public key formats supported
- PKCS#1 RSAPublicKey* (PEM header: BEGIN RSA PUBLIC KEY)
- X.509 SubjectPublicKeyInfo** (PEM header: BEGIN PUBLIC KEY)
- **OpenSSH line format is not supported.**
Private key formats supported (unencrypted)
- PKCS#1 RSAPrivateKey** (PEM header: BEGIN RSA PRIVATE KEY)
- PKCS#8 PrivateKeyInfo* (PEM header: BEGIN PRIVATE KEY)
## Creating the First User Key ## Creating the First User Key
When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the superuser) must create a user key. This can be done by navigating to Profile > User Key. When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the superuser) must create a user key. This can be done by navigating to Profile > User Key.

View File

@ -40,6 +40,8 @@ Racks can be arranged into groups. As with sites, how you choose to designate ra
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. Hierarchical recursion of rack groups is not currently supported.
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.)
## Rack Roles ## Rack Roles
Each rack can optionally be assigned a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable. Each rack can optionally be assigned a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable.

View File

@ -69,6 +69,14 @@ If the new field will be included in the object list view, add a column to the m
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated. Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
### 11. Adjust API and model tests ### 11. Create/extend test cases
Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including:
* API serializer/view tests
* Filter tests
* Form tests
* Model tests
* View tests
Be diligent to ensure all of the relevant test suites are adapted or extended as necessary to test any new functionality.

View File

@ -33,6 +33,10 @@ 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`.

View File

@ -0,0 +1,168 @@
# 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

@ -32,7 +32,7 @@ pycodestyle --ignore=W504,E501 netbox/
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and attacks. The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and attacks.
If there's a strong case for introducing a new depdency, it must meet the following criteria: If there's a strong case for introducing a new dependency, it must meet the following criteria:
* Its complete source code must be published and freely accessible without registration. * Its complete source code must be published and freely accessible without registration.
* Its license must be conducive to inclusion in an open source project. * Its license must be conducive to inclusion in an open source project.
@ -45,10 +45,18 @@ When adding a new dependency, a short description of the package and the URL of
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point. * When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point.
* Prioritize readability over concision. Python is a very flexible language that typically gives us several options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely. * No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely.
* Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable. * Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
* Every model should have a docstring. Every custom method should include an expalantion of its function. * Every model should have a docstring. Every custom method should include an explanation of its function.
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`. * Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
## Branding
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. But never "Netbox" or any other deviation.
* There is an SVG form of the NetBox logo at [docs/netbox_logo.svg](../netbox_logo.svg). It is preferred to use this logo for all purposes as it scales to arbitrary sizes without loss of resolution. If a raster image is required, the SVG logo should be converted to a PNG image of the prescribed size.

View File

@ -4,7 +4,7 @@ NetBox requires a PostgreSQL database to store data. This can be hosted locally
The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
!!! warning !!! warning
NetBox v2.2 and later requires PostgreSQL 9.4 or higher. NetBox requires PostgreSQL 9.4 or higher.
# Installation # Installation

View File

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

View File

@ -1,4 +1,4 @@
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence. We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll use systemd to enable service persistence.
!!! 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.
@ -29,7 +29,7 @@ server {
location / { location / {
proxy_pass http://127.0.0.1:8001; proxy_pass http://127.0.0.1:8001;
proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
@ -99,6 +99,9 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
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). 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
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 Installation # gunicorn Installation
Install gunicorn: Install gunicorn:
@ -107,47 +110,54 @@ Install gunicorn:
# pip3 install gunicorn # pip3 install gunicorn
``` ```
Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`. More info on `max_requests` can be found in the [gunicorn docs](https://docs.gunicorn.org/en/stable/settings.html#max-requests). 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.
```no-highlight ```no-highlight
command = '/usr/bin/gunicorn' # cd /opt/netbox
pythonpath = '/opt/netbox/netbox' # cp contrib/gunicorn.py /opt/netbox/gunicorn.py
bind = '127.0.0.1:8001'
workers = 3
user = 'www-data'
max_requests = 5000
max_requests_jitter = 500
``` ```
# supervisord Installation You may wish to edit this file to change the bound IP address or port number, or to make performance-related adjustments.
Install supervisor: # systemd configuration
We'll use systemd to control the daemonization of NetBox services. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory:
```no-highlight ```no-highlight
# apt-get install -y supervisor # cp contrib/*.service /etc/systemd/system/
``` ```
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`. !!! note
These service files assume that gunicorn is installed at `/usr/local/bin/gunicorn`. If the output of `which gunicorn` indicates a different path, you'll need to correct the `ExecStart` path in both files.
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight ```no-highlight
[program:netbox] # systemctl daemon-reload
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi # systemctl start netbox.service
directory = /opt/netbox/netbox/ # systemctl start netbox-rq.service
user = www-data # systemctl enable netbox.service
# systemctl enable netbox-rq.service
[program:netbox-rqworker]
command = python3 /opt/netbox/netbox/manage.py rqworker
directory = /opt/netbox/netbox/
user = www-data
``` ```
Then, restart the supervisor service to detect and run the gunicorn service: You can use the command `systemctl status netbox` to verify that the WSGI service is running:
```no-highlight ```
# service supervisor restart # systemctl status netbox.service
● netbox.service - NetBox WSGI Service
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2019-12-12 19:23:40 UTC; 25s ago
Docs: https://netbox.readthedocs.io/en/stable/
Main PID: 11993 (gunicorn)
Tasks: 6 (limit: 2362)
CGroup: /system.slice/netbox.service
├─11993 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
├─12015 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
├─12016 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
...
``` ```
At this point, you should be able to connect to the nginx HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running. At this point, you should be able to connect to the HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running.
!!! info !!! info
Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment. Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You may want to make adjustments to better suit your production environment.

View File

@ -110,8 +110,8 @@ AUTH_LDAP_USER_FLAGS_BY_GROUP = {
AUTH_LDAP_FIND_GROUP_PERMS = True AUTH_LDAP_FIND_GROUP_PERMS = True
# Cache groups for one hour to reduce LDAP traffic # Cache groups for one hour to reduce LDAP traffic
AUTH_LDAP_CACHE_GROUPS = True AUTH_LDAP_CACHE_TIMEOUT = 3600
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
``` ```
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in. * `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.

View File

@ -4,7 +4,7 @@ The following sections detail how to set up a new instance of NetBox:
1. [PostgreSQL database](1-postgresql.md) 1. [PostgreSQL database](1-postgresql.md)
2. [NetBox components](2-netbox.md) 2. [NetBox components](2-netbox.md)
3. [HTTP dameon](3-http-daemon.md) 3. [HTTP daemon](3-http-daemon.md)
4. [LDAP authentication](4-ldap.md) (optional) 4. [LDAP authentication](4-ldap.md) (optional)
# Upgrading # Upgrading
@ -12,3 +12,5 @@ The following sections detail how to set up a new instance of NetBox:
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md). If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
NetBox v2.5 and later requires Python 3.5 or higher. Please see the instructions for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2. NetBox v2.5 and later requires Python 3.5 or higher. Please see the instructions for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.
Netbox v2.5.9 and later moved to using systemd instead of supervisord. Please see the instructions for [migrating to systemd](migrating-to-systemd.md) if you are still using supervisord.

View File

@ -0,0 +1,57 @@
# Migration
Migration is not required, as supervisord will still continue to function.
## Ubuntu
### Remove supervisord:
```no-highlight
# apt-get remove -y supervisord
```
### systemd configuration:
We'll use systemd to control the daemonization of NetBox services. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory:
```no-highlight
# cp contrib/*.service /etc/systemd/system/
```
!!! note
These service files assume that gunicorn is installed at `/usr/local/bin/gunicorn`. If the output of `which gunicorn` indicates a different path, you'll need to correct the `ExecStart` path in both files.
!!! note
You may need to modify the user that the systemd service runs as. Please verify the user for httpd on your specific release and edit both files to match your httpd service under user and group. The username could be "nobody", "nginx", "apache", "www-data" or any number of other usernames.
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight
# systemctl daemon-reload
# systemctl start netbox.service
# systemctl start netbox-rq.service
# systemctl enable netbox.service
# systemctl enable netbox-rq.service
```
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
```
# systemctl status netbox.service
● netbox.service - NetBox WSGI Service
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2019-12-12 19:23:40 UTC; 25s ago
Docs: https://netbox.readthedocs.io/en/stable/
Main PID: 11993 (gunicorn)
Tasks: 6 (limit: 2362)
CGroup: /system.slice/netbox.service
├─11993 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
├─12015 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
├─12016 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
...
```
At this point, you should be able to connect to the HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running.
!!! info
Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You may want to make adjustments to better suit your production environment.

View File

@ -84,14 +84,12 @@ This script:
# Restart the WSGI Service # Restart the WSGI Service
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`: Finally, restart the WSGI services to run the new code. If you followed this guide for the initial installation, this is done using `systemctl:
```no-highlight ```no-highlight
# sudo supervisorctl restart netbox # sudo systemctl restart netbox
# sudo systemctl restart netbox-rq
``` ```
If using webhooks, also restart the Redis worker: !!! note
It's possible you are still using supervisord instead of the linux native systemd. If you are still using supervisord you can restart the services by either restarting supervisord or by using supervisorctl to restart netbox.
```no-highlight
# sudo supervisorctl restart netbox-rqworker
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 339 KiB

View File

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

View File

@ -1,3 +1,40 @@
# v2.6.12 (2020-01-13)
## Enhancements
* [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger (OpenAPI)
* [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering over the link
* [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers
* [#2598](https://github.com/netbox-community/netbox/issues/2598) - Toggle the display of child prefixes/IP addresses
* [#3009](https://github.com/netbox-community/netbox/issues/3009) - Search by description when assigning IP address to interfaces
* [#3021](https://github.com/netbox-community/netbox/issues/3021) - Add `tenant` filter field for cables
* [#3090](https://github.com/netbox-community/netbox/issues/3090) - Enable filtering of interfaces by name on the device view
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations view
* [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate assigned circuits at the provider details view
* [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total path length to cable trace
* [#3491](https://github.com/netbox-community/netbox/issues/3491) - Include content of response on webhook error
* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Enable word expansion during interface creation
* [#3668](https://github.com/netbox-community/netbox/issues/3668) - Enable searching by DNS name when assigning IP address
* [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms
* [#3891](https://github.com/netbox-community/netbox/issues/3891) - Add `local_context_data` filter for virtual machines
## Bug Fixes
* [#3589](https://github.com/netbox-community/netbox/issues/3589) - Fix validation on tagged VLANs of an interface
* [#3849](https://github.com/netbox-community/netbox/issues/3849) - Fix ordering of models when dumping data to JSON
* [#3853](https://github.com/netbox-community/netbox/issues/3853) - Fix device role link on config context view
* [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses
* [#3857](https://github.com/netbox-community/netbox/issues/3857) - Fix rendering of grouped custom links
* [#3862](https://github.com/netbox-community/netbox/issues/3862) - Allow filtering device components by multiple device names
* [#3864](https://github.com/netbox-community/netbox/issues/3864) - Disallow /0 masks for prefixes and IP addresses
* [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs on the IP address view
* [#3876](https://github.com/netbox-community/netbox/issues/3876) - Fix minimum/maximum value rendering for site ASN field
* [#3882](https://github.com/netbox-community/netbox/issues/3882) - Fix filtering of devices by rack group
* [#3898](https://github.com/netbox-community/netbox/issues/3898) - Fix references to deleted cables without a label
* [#3905](https://github.com/netbox-community/netbox/issues/3905) - Fix divide-by-zero on power feeds with low power values
---
# v2.6.11 (2020-01-03) # v2.6.11 (2020-01-03)
## Bug Fixes ## Bug Fixes

View File

@ -0,0 +1,476 @@
# v2.7.8 (2020-02-25)
## Enhancements
* [#3145](https://github.com/netbox-community/netbox/issues/3145) - Add a "decommissioning" cable status
* [#4173](https://github.com/netbox-community/netbox/issues/4173) - Return graceful error message when webhook queuing fails
* [#4227](https://github.com/netbox-community/netbox/issues/4227) - Omit internal fields from the change log data
* [#4237](https://github.com/netbox-community/netbox/issues/4237) - Support Jinja2 templating for webhook payload and headers
* [#4262](https://github.com/netbox-community/netbox/issues/4262) - Extend custom scripts to pass the `commit` value via `run()`
* [#4267](https://github.com/netbox-community/netbox/issues/4267) - Denote rack role on rack elevations list
## Bug Fixes
* [#4221](https://github.com/netbox-community/netbox/issues/4221) - Fix exception when deleting a device with interface connections when an interfaces webhook is defined
* [#4222](https://github.com/netbox-community/netbox/issues/4222) - Escape double quotes on encapsulated values during CSV export
* [#4224](https://github.com/netbox-community/netbox/issues/4224) - Fix display of rear device image if front image is not defined
* [#4228](https://github.com/netbox-community/netbox/issues/4228) - Improve fit of device images in rack elevations
* [#4230](https://github.com/netbox-community/netbox/issues/4230) - Fix rack units filtering on elevation endpoint
* [#4232](https://github.com/netbox-community/netbox/issues/4232) - Enforce consistent background striping in rack elevations
* [#4235](https://github.com/netbox-community/netbox/issues/4235) - Fix API representation of `content_type` for export templates
* [#4239](https://github.com/netbox-community/netbox/issues/4239) - Fix exception when selecting all filtered objects during bulk edit
* [#4240](https://github.com/netbox-community/netbox/issues/4240) - Fix exception when filtering foreign keys by NULL
* [#4241](https://github.com/netbox-community/netbox/issues/4241) - Correct IP address hyperlinks on interface view
* [#4246](https://github.com/netbox-community/netbox/issues/4246) - Fix duplication of field attributes when multiple IPNetworkVars are present in a script
* [#4252](https://github.com/netbox-community/netbox/issues/4252) - Fix power port assignment for power outlet templates created via REST API
* [#4272](https://github.com/netbox-community/netbox/issues/4272) - Interface type should be required by API serializer
---
# v2.7.7 (2020-02-20)
**Note:** This release fixes a bug affecting the natural ordering of interfaces. If any interfaces appear unordered in
NetBox, run the following management command to recalculate their naturalized values after upgrading:
```
python3 manage.py renaturalize dcim.Interface
```
## Enhancements
* [#1529](https://github.com/netbox-community/netbox/issues/1529) - Enable display of device images in rack elevations
* [#2511](https://github.com/netbox-community/netbox/issues/2511) - Compare object change to the previous change
* [#3810](https://github.com/netbox-community/netbox/issues/3810) - Preserve slug value when editing existing objects
* [#3840](https://github.com/netbox-community/netbox/issues/3840) - Enhance search function when selecting VLANs for interface assignment
* [#4170](https://github.com/netbox-community/netbox/issues/4170) - Improve color contrast in rack elevation drawings
* [#4206](https://github.com/netbox-community/netbox/issues/4206) - Add RJ-11 console port type
* [#4209](https://github.com/netbox-community/netbox/issues/4209) - Enable filtering interfaces list view by enabled
## Bug Fixes
* [#2519](https://github.com/netbox-community/netbox/issues/2519) - Avoid race condition when provisioning "next available" IPs/prefixes via the API
* [#3967](https://github.com/netbox-community/netbox/issues/3967) - Fix missing migration for interface templates of type "other"
* [#4168](https://github.com/netbox-community/netbox/issues/4168) - Role is not required when creating a virtual machine
* [#4175](https://github.com/netbox-community/netbox/issues/4175) - Fix potential exception when bulk editing objects from a filtered list
* [#4179](https://github.com/netbox-community/netbox/issues/4179) - Site is required when creating a rack group or power panel
* [#4183](https://github.com/netbox-community/netbox/issues/4183) - Fix representation of NaturalOrderingField values in change log
* [#4194](https://github.com/netbox-community/netbox/issues/4194) - Role field should not be required when searching/filtering secrets
* [#4196](https://github.com/netbox-community/netbox/issues/4196) - Fix exception when viewing LLDP neighbors page
* [#4202](https://github.com/netbox-community/netbox/issues/4202) - Prevent reassignment to master device when bulk editing VC member interfaces
* [#4204](https://github.com/netbox-community/netbox/issues/4204) - Fix assignment of mask length when bulk editing prefixes
* [#4211](https://github.com/netbox-community/netbox/issues/4211) - Include trailing text when naturalizing interface names
* [#4213](https://github.com/netbox-community/netbox/issues/4213) - Restore display of tags and custom fields on power feed view
---
# v2.7.6 (2020-02-13)
## Bug Fixes
* [#4166](https://github.com/netbox-community/netbox/issues/4166) - Fix schema migrations to enforce maximum character length for naturalized fields
---
# v2.7.5 (2020-02-13)
**Note:** This release includes several database schema migrations that calculate and store copies of names for certain objects to improve natural ordering performance (see [#3799](https://github.com/netbox-community/netbox/issues/3799)). These migrations may take a few minutes to run if you have a very large number of objects defined in NetBox.
## Enhancements
* [#3766](https://github.com/netbox-community/netbox/issues/3766) - Allow custom script authors to specify the form widget for each variable
* [#3799](https://github.com/netbox-community/netbox/issues/3799) - Greatly improve performance when ordering device components
* [#3984](https://github.com/netbox-community/netbox/issues/3984) - Add support for Redis Sentinel
* [#3986](https://github.com/netbox-community/netbox/issues/3986) - Include position numbers in SVG image when rendering rack elevations
* [#4093](https://github.com/netbox-community/netbox/issues/4093) - Add more status choices for virtual machines
* [#4100](https://github.com/netbox-community/netbox/issues/4100) - Add device filter to component list views
* [#4113](https://github.com/netbox-community/netbox/issues/4113) - Add bulk edit functionality for device type components
* [#4116](https://github.com/netbox-community/netbox/issues/4116) - Enable bulk edit and delete functions for device component list views
* [#4129](https://github.com/netbox-community/netbox/issues/4129) - Add buttons to delete individual device type components
## Bug Fixes
* [#3507](https://github.com/netbox-community/netbox/issues/3507) - Fix filtering IP addresses by multiple devices
* [#3995](https://github.com/netbox-community/netbox/issues/3995) - Make dropdown menus in the navigation bar scrollable on small screens
* [#4083](https://github.com/netbox-community/netbox/issues/4083) - Permit nullifying applicable choice fields via API requests
* [#4089](https://github.com/netbox-community/netbox/issues/4089) - Selection of power outlet type during bulk update is optional
* [#4090](https://github.com/netbox-community/netbox/issues/4090) - Render URL custom fields as links under object view
* [#4091](https://github.com/netbox-community/netbox/issues/4091) - Fix filtering of objects by custom fields using UI search form
* [#4099](https://github.com/netbox-community/netbox/issues/4099) - Linkify interfaces on global interfaces list
* [#4108](https://github.com/netbox-community/netbox/issues/4108) - Avoid extraneous database queries when rendering search forms
* [#4134](https://github.com/netbox-community/netbox/issues/4134) - Device power ports and outlets should inherit type from the parent device type
* [#4137](https://github.com/netbox-community/netbox/issues/4137) - Disable occupied terminations when connecting a cable to a circuit
* [#4138](https://github.com/netbox-community/netbox/issues/4138) - Restore device bay counts in rack elevation diagrams
* [#4146](https://github.com/netbox-community/netbox/issues/4146) - Fix enforcement of secret role assignment for secret decryption
* [#4150](https://github.com/netbox-community/netbox/issues/4150) - Correct YAML rendering of config contexts
* [#4159](https://github.com/netbox-community/netbox/issues/4159) - Fix implementation of Redis caching configuration
---
# v2.7.4 (2020-02-04)
## Enhancements
* [#568](https://github.com/netbox-community/netbox/issues/568) - Allow custom fields to be imported and exported using CSV
* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget
* [#3313](https://github.com/netbox-community/netbox/issues/3313) - Toggle config context display between JSON and YAML
* [#3886](https://github.com/netbox-community/netbox/issues/3886) - Enable assigning config contexts by cluster and cluster group
* [#4051](https://github.com/netbox-community/netbox/issues/4051) - Disable the `makemigrations` management command
## Bug Fixes
* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when bulk editing interfaces (revised)
* [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts
* [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer
* [#4052](https://github.com/netbox-community/netbox/issues/4052) - Fix error when bulk importing interfaces to virtual machines
* [#4056](https://github.com/netbox-community/netbox/issues/4056) - Repair schema migration for Rack.outer_unit (from #3569)
* [#4067](https://github.com/netbox-community/netbox/issues/4067) - Correct permission checked when creating a rack (vs. editing)
* [#4071](https://github.com/netbox-community/netbox/issues/4071) - Enforce "view tag" permission on individual tag view
* [#4079](https://github.com/netbox-community/netbox/issues/4079) - Fix assignment of power panel when bulk editing power feeds
* [#4084](https://github.com/netbox-community/netbox/issues/4084) - Fix exception when creating an interface with tagged VLANs
---
# v2.7.3 (2020-01-28)
## Enhancements
* [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable
* [#3338](https://github.com/netbox-community/netbox/issues/3338) - Include circuit terminations in API representation of circuits
* [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts
* [#3978](https://github.com/netbox-community/netbox/issues/3978) - Add VRF filtering to search NAT IP
* [#4005](https://github.com/netbox-community/netbox/issues/4005) - Include timezone context in webhook timestamps
## Bug Fixes
* [#3950](https://github.com/netbox-community/netbox/issues/3950) - Automatically select parent manufacturer when specifying initial device type during device creation
* [#3982](https://github.com/netbox-community/netbox/issues/3982) - Restore tooltip for reservations on rack elevations
* [#3983](https://github.com/netbox-community/netbox/issues/3983) - Permit the creation of multiple unnamed devices
* [#3989](https://github.com/netbox-community/netbox/issues/3989) - Correct HTTP content type assignment for webhooks
* [#3999](https://github.com/netbox-community/netbox/issues/3999) - Do not filter child results by null if non-required parent fields are blank
* [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings
* [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form
* [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations
* [#4022](https://github.com/netbox-community/netbox/issues/4022) - Fix display of assigned IPs when filtering device interfaces
* [#4025](https://github.com/netbox-community/netbox/issues/4025) - Correct display of cable status (various places)
* [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status
* [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs
* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when setting interfaces to tagged mode in bulk
* [#4033](https://github.com/netbox-community/netbox/issues/4033) - Restore missing comments field label of various bulk edit forms
---
# v2.7.2 (2020-01-21)
## Enhancements
* [#3135](https://github.com/netbox-community/netbox/issues/3135) - Documented power modelling
* [#3842](https://github.com/netbox-community/netbox/issues/3842) - Add 802.11ax interface type
* [#3954](https://github.com/netbox-community/netbox/issues/3954) - Add `device_bays` filter for devices and device types
## Bug Fixes
* [#3721](https://github.com/netbox-community/netbox/issues/3721) - Allow Unicode characters in tag slugs
* [#3923](https://github.com/netbox-community/netbox/issues/3923) - Indicate validation failure when using SSH-style RSA keys
* [#3951](https://github.com/netbox-community/netbox/issues/3951) - Fix exception in webhook worker due to missing constant
* [#3953](https://github.com/netbox-community/netbox/issues/3953) - Fix validation error when creating child devices
* [#3960](https://github.com/netbox-community/netbox/issues/3960) - Fix legacy device status choice
* [#3962](https://github.com/netbox-community/netbox/issues/3962) - Fix display of unnamed devices in rack elevations
* [#3963](https://github.com/netbox-community/netbox/issues/3963) - Restore tooltip for devices in rack elevations
* [#3964](https://github.com/netbox-community/netbox/issues/3964) - Show borders around devices in rack elevations
* [#3965](https://github.com/netbox-community/netbox/issues/3965) - Indicate the presence of "background" devices in rack elevations
* [#3966](https://github.com/netbox-community/netbox/issues/3966) - Fix filtering of device components by region/site
* [#3967](https://github.com/netbox-community/netbox/issues/3967) - Resolve migration of "other" interface type
---
# v2.7.1 (2020-01-16)
## Bug Fixes
* [#3941](https://github.com/netbox-community/netbox/issues/3941) - Fixed exception when attempting to assign IP to interface
* [#3943](https://github.com/netbox-community/netbox/issues/3943) - Prevent rack elevation links from opening new tabs/windows
* [#3944](https://github.com/netbox-community/netbox/issues/3944) - Fix AttributeError exception when viewing prefixes list
---
# v2.7.0 (2020-01-16)
**Note:** This release completely removes the topology map feature ([#2745](https://github.com/netbox-community/netbox/issues/2745)).
**Note:** NetBox v2.7 is the last major release that will support Python 3.5. Beginning with NetBox v2.8, Python 3.6 or
higher will be required.
## New Features
### Enhanced Device Type Import ([#451](https://github.com/netbox-community/netbox/issues/451))
NetBox now supports the import of device types and related component templates using definitions written in YAML or
JSON. For example, the following will create a new device type with four network interfaces, two power ports, and a
console port:
```yaml
manufacturer: Acme
model: Packet Shooter 9000
slug: packet-shooter-9000
u_height: 1
interfaces:
- name: ge-0/0/0
type: 1000base-t
- name: ge-0/0/1
type: 1000base-t
- name: ge-0/0/2
type: 1000base-t
- name: ge-0/0/3
type: 1000base-t
power-ports:
- name: PSU0
- name: PSU1
console-ports:
- name: Console
```
This new functionality replaces the old CSV-based import form, which did not allow for bulk import of component
templates.
### Bulk Import of Device Components ([#822](https://github.com/netbox-community/netbox/issues/822))
Device components such as console ports, power ports, and interfaces can now be imported in bulk to multiple devices in
CSV format. Here's an example showing the bulk import of interfaces to several devices:
```
device,name,type
Switch1,Vlan100,Virtual
Switch1,Vlan200,Virtual
Switch2,Vlan100,Virtual
Switch2,Vlan200,Virtual
```
The import form for each type of device component is available under the "Devices" item in the navigation menu.
### External File Storage ([#1814](https://github.com/netbox-community/netbox/issues/1814))
In prior releases, the only option for storing uploaded files (e.g. image attachments) was to save them to the local
filesystem on the NetBox server. This release introduces support for several remote storage backends provided by the
[`django-storages`](https://django-storages.readthedocs.io/en/stable/) library. These include:
* Amazon S3
* ApacheLibcloud
* Azure Storage
* netbox-community Spaces
* Dropbox
* FTP
* Google Cloud Storage
* SFTP
To enable remote file storage, first install the `django-storages` package:
```
pip install django-storages
```
Then, set the appropriate storage backend and its configuration in `configuration.py`. Here's an example using Amazon
S3:
```python
STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'
STORAGE_CONFIG = {
'AWS_ACCESS_KEY_ID': '<Key>',
'AWS_SECRET_ACCESS_KEY': '<Secret>',
'AWS_STORAGE_BUCKET_NAME': 'netbox',
'AWS_S3_REGION_NAME': 'eu-west-1',
}
```
Thanks to [@steffann](https://github.com/steffann) for contributing this work!
### Rack Elevations Rendered via SVG ([#2248](https://github.com/netbox-community/netbox/issues/2248))
NetBox v2.7 introduces a new method of rendering rack elevations as an
[SVG image](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) via a REST API endpoint. This replaces the prior
method of rendering elevations using pure HTML and CSS, which was cumbersome and had several shortcomings. Rendering
rack elevations as SVG images via the REST API allows users to retrieve and make use of the drawings in their own
tooling. This also opens the door to other feature requests related to rack elevations in the NetBox backlog.
This feature implements a new REST API endpoint:
```
/api/dcim/racks/<id>/elevation/
```
By default, this endpoint returns a paginated JSON response representing each rack unit in the given elevation. This is
the same response returned by the existing rack units detail endpoint at `/api/dcim/racks/<id>/units/`, which will be
removed in v2.8 (see [#3753](https://github.com/netbox-community/netbox/issues/3753)).
To render the elevation as an SVG image, include the `render=svg` query parameter in the request. You may also control
the width and height of the elevation drawing (in pixels) by passing the `unit_width` and `unit_height` parameters. (The
default values for these parameters are 230 and 20, respectively.) Additionally, the `face` parameter may be used to
request either the `front` or `rear` of the elevation. Below is in example request:
```
/api/dcim/racks/<id>/elevation/?render=svg&face=rear&unit_width=300&unit_height=35
```
Thanks to [@hellerve](https://github.com/hellerve) for doing the heavy lifting on this!
## Changes
### Topology Maps Removed ([#2745](https://github.com/netbox-community/netbox/issues/2745))
The topology maps feature has been removed to help focus NetBox development efforts. Please replicate any required data
to another source before upgrading NetBox to v2.7, as any existing topology maps will be deleted.
### Supervisor Replaced with systemd ([#2902](https://github.com/netbox-community/netbox/issues/2902))
The NetBox [installation documentation](https://netbox.readthedocs.io/en/stable/installation/) has been updated to
provide instructions for managing the WSGI and RQ services using systemd instead of supervisor. This removes the need to
install supervisor and simplifies administration of the processes.
### Redis Configuration ([#3282](https://github.com/netbox-community/netbox/issues/3282))
NetBox v2.6 introduced request caching and added the `CACHE_DATABASE` option to the existing `REDIS` database
configuration parameter. This did not, however, allow for using two different Redis connections for the separate caching
and webhook queuing functions. This release modifies the `REDIS` parameter to accept two discrete subsections named
`webhooks` and `caching`. This requires modification of the `REDIS` parameter in `configuration.py` as follows:
Old Redis configuration:
```python
REDIS = {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 0,
'CACHE_DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
```
New Redis configuration:
```python
REDIS = {
'webhooks': {
'HOST': 'redis.example.com',
'PORT': 1234,
'PASSWORD': 'foobar',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
}
```
Note that the `CACHE_DATABASE` parameter has been removed and the connection settings have been duplicated for both
`webhooks` and `caching`. This allows the user to make use of separate Redis instances if desired. It is fine to use the
same Redis service for both functions, although the database identifiers should be different.
### WEBHOOKS_ENABLED Configuration Setting Removed ([#3408](https://github.com/netbox-community/netbox/issues/3408))
As `django-rq` is now a required library, NetBox assumes that the RQ worker process is running. The installation and
upgrade documentation has been updated to reflect this, and the `WEBHOOKS_ENABLED` configuration parameter is no longer
used. Please ensure that both the NetBox WSGI service and the RQ worker process are running on all production
installations.
### API Choice Fields Now Use String Values ([#3569](https://github.com/netbox-community/netbox/issues/3569))
NetBox's REST API presents fields which reference a particular choice as a dictionary with two keys: `value` and
`label`. In previous versions, `value` was an integer which represented a particular choice in the database. This has
been changed to a more human-friendly "slug" string, which is essentially a simplified version of the choice's `label`.
For example, The site model's `status` field was previously represented as:
```json
"status": {
"value": 1,
"label": "Active"
},
```
In NetBox v2.7, it now looks like this:
```json
"status": {
"value": "active",
"label": "Active",
"id": 1
},
```
This change allows for much more intuitive representation and manipulation of values, and removes the need for API
consumers to maintain local mappings of static integer values.
Note that that all v2.7 releases will continue to accept the legacy integer values in write requests (`POST`, `PUT`, and
`PATCH`) to maintain backward compatibility. Additionally, the legacy numeric identifier is conveyed in the `id` field
for convenient reference as consumers adopt to the new string values. This behavior will be discontinued in NetBox v2.8.
## Enhancements
* [#33](https://github.com/netbox-community/netbox/issues/33) - Add ability to clone objects (pre-populate form fields)
* [#648](https://github.com/netbox-community/netbox/issues/648) - Pre-populate form fields when selecting "create and
add another"
* [#792](https://github.com/netbox-community/netbox/issues/792) - Add power port and power outlet types
* [#1865](https://github.com/netbox-community/netbox/issues/1865) - Add console port and console server port types
* [#2669](https://github.com/netbox-community/netbox/issues/2669) - Relax uniqueness constraint on device and VM names
* [#2902](https://github.com/netbox-community/netbox/issues/2902) - Replace `supervisord` with `systemd`
* [#3455](https://github.com/netbox-community/netbox/issues/3455) - Add tenant assignment to virtual machine clusters
* [#3520](https://github.com/netbox-community/netbox/issues/3520) - Add Jinja2 template support for graphs
* [#3525](https://github.com/netbox-community/netbox/issues/3525) - Enable IP address filtering using multiple address
parameters
* [#3564](https://github.com/netbox-community/netbox/issues/3564) - Add list views for all device components
* [#3538](https://github.com/netbox-community/netbox/issues/3538) - Introduce a REST API endpoint for executing custom
scripts
* [#3655](https://github.com/netbox-community/netbox/issues/3655) - Add `description` field to organizational models
* [#3664](https://github.com/netbox-community/netbox/issues/3664) - Enable applying configuration contexts by tags
* [#3706](https://github.com/netbox-community/netbox/issues/3706) - Increase `available_power` maximum value on
PowerFeed
* [#3731](https://github.com/netbox-community/netbox/issues/3731) - Change Graph.type to a ContentType foreign key field
* [#3801](https://github.com/netbox-community/netbox/issues/3801) - Use YAML for export of device types
## Bug Fixes
* [#3830](https://github.com/netbox-community/netbox/issues/3830) - Ensure deterministic ordering for all models
* [#3900](https://github.com/netbox-community/netbox/issues/3900) - Fix exception when deleting device types
* [#3914](https://github.com/netbox-community/netbox/issues/3914) - Fix interface filter field when unauthenticated
* [#3919](https://github.com/netbox-community/netbox/issues/3919) - Fix utilization graph extending out of bounds when
utilization > 100%
* [#3927](https://github.com/netbox-community/netbox/issues/3927) - Fix exception when deleting devices with secrets
assigned
* [#3930](https://github.com/netbox-community/netbox/issues/3930) - Fix API rendering of the `family` field for
aggregates
## Bug Fixes (From Beta)
* [#3868](https://github.com/netbox-community/netbox/issues/3868) - Fix creation of interfaces for virtual machines
* [#3878](https://github.com/netbox-community/netbox/issues/3878) - Fix database migration for cable status field
## API Changes
* Choice fields now use human-friendly strings for their values instead of integers (see
[#3569](https://github.com/netbox-community/netbox/issues/3569)).
* Introduced the `/api/extras/scripts/` endpoint for retrieving and executing custom scripts
* circuits.CircuitType: Added field `description`
* dcim.ConsolePort: Added field `type`
* dcim.ConsolePortTemplate: Added field `type`
* dcim.ConsoleServerPort: Added field `type`
* dcim.ConsoleServerPortTemplate: Added field `type`
* dcim.DeviceRole: Added field `description`
* dcim.PowerPort: Added field `type`
* dcim.PowerPortTemplate: Added field `type`
* dcim.PowerOutlet: Added field `type`
* dcim.PowerOutletTemplate: Added field `type`
* dcim.RackRole: Added field `description`
* extras.Graph: Added field `template_language` (to indicate `django` or `jinja2`)
* extras.Graph: The `type` field has been changed to a content type foreign key. Models are specified as
`<app>.<model>`; e.g. `dcim.site`.
* ipam.Role: Added field `description`
* secrets.SecretRole: Added field `description`
* virtualization.Cluster: Added field `tenant`

View File

@ -12,6 +12,7 @@ pages:
- 4. LDAP (Optional): 'installation/4-ldap.md' - 4. LDAP (Optional): 'installation/4-ldap.md'
- Upgrading NetBox: 'installation/upgrading.md' - Upgrading NetBox: 'installation/upgrading.md'
- Migrating to Python3: 'installation/migrating-to-python3.md' - Migrating to Python3: 'installation/migrating-to-python3.md'
- Migrating to systemd: 'installation/migrating-to-systemd.md'
- Configuration: - Configuration:
- Configuring NetBox: 'configuration/index.md' - Configuring NetBox: 'configuration/index.md'
- Required Settings: 'configuration/required-settings.md' - Required Settings: 'configuration/required-settings.md'
@ -24,6 +25,7 @@ pages:
- Virtual Machines: 'core-functionality/virtual-machines.md' - Virtual Machines: 'core-functionality/virtual-machines.md'
- Services: 'core-functionality/services.md' - Services: 'core-functionality/services.md'
- Circuits: 'core-functionality/circuits.md' - Circuits: 'core-functionality/circuits.md'
- Power: 'core-functionality/power.md'
- Secrets: 'core-functionality/secrets.md' - Secrets: 'core-functionality/secrets.md'
- Tenancy: 'core-functionality/tenancy.md' - Tenancy: 'core-functionality/tenancy.md'
- Additional Features: - Additional Features:
@ -35,10 +37,10 @@ pages:
- Custom Scripts: 'additional-features/custom-scripts.md' - Custom Scripts: 'additional-features/custom-scripts.md'
- Export Templates: 'additional-features/export-templates.md' - Export Templates: 'additional-features/export-templates.md'
- Graphs: 'additional-features/graphs.md' - Graphs: 'additional-features/graphs.md'
- NAPALM: 'additional-features/napalm.md'
- Prometheus Metrics: 'additional-features/prometheus-metrics.md' - Prometheus Metrics: 'additional-features/prometheus-metrics.md'
- Reports: 'additional-features/reports.md' - Reports: 'additional-features/reports.md'
- Tags: 'additional-features/tags.md' - Tags: 'additional-features/tags.md'
- Topology Maps: 'additional-features/topology-maps.md'
- Webhooks: 'additional-features/webhooks.md' - Webhooks: 'additional-features/webhooks.md'
- Administration: - Administration:
- Replicating NetBox: 'administration/replicating-netbox.md' - Replicating NetBox: 'administration/replicating-netbox.md'
@ -54,7 +56,9 @@ pages:
- Utility Views: 'development/utility-views.md' - Utility Views: 'development/utility-views.md'
- Extending Models: 'development/extending-models.md' - Extending Models: 'development/extending-models.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.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'
- Version 2.4: 'release-notes/version-2.4.md' - Version 2.4: 'release-notes/version-2.4.md'

View File

@ -1,13 +1,13 @@
from rest_framework import serializers from rest_framework import serializers
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from circuits.constants import CIRCUIT_STATUS_CHOICES from circuits.choices import CircuitStatusChoices
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
from dcim.api.serializers import ConnectedEndpointSerializer from dcim.api.serializers import ConnectedEndpointSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import ChoiceField, ValidatedModelSerializer from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
from .nested_serializers import * from .nested_serializers import *
@ -36,21 +36,33 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'name', 'slug', 'circuit_count'] fields = ['id', 'name', 'slug', 'description', 'circuit_count']
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer()
connected_endpoint = NestedInterfaceSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id']
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer): class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
provider = NestedProviderSerializer() provider = NestedProviderSerializer()
status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False) status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = NestedCircuitTypeSerializer() type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
class Meta: class Meta:
model = Circuit model = Circuit
fields = [ fields = [
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]

View File

@ -15,15 +15,15 @@ router = routers.DefaultRouter()
router.APIRootView = CircuitsRootView router.APIRootView = CircuitsRootView
# Field choices # Field choices
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice') router.register('_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice')
# Providers # Providers
router.register(r'providers', views.ProviderViewSet) router.register('providers', views.ProviderViewSet)
# Circuits # Circuits
router.register(r'circuit-types', views.CircuitTypeViewSet) router.register('circuit-types', views.CircuitTypeViewSet)
router.register(r'circuits', views.CircuitViewSet) router.register('circuits', views.CircuitViewSet)
router.register(r'circuit-terminations', views.CircuitTerminationViewSet) router.register('circuit-terminations', views.CircuitTerminationViewSet)
app_name = 'circuits-api' app_name = 'circuits-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -7,7 +7,7 @@ from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit 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, GRAPH_TYPE_PROVIDER from extras.models import Graph
from utilities.api import FieldChoicesViewSet, ModelViewSet from utilities.api import FieldChoicesViewSet, ModelViewSet
from . import serializers from . import serializers
@ -18,8 +18,8 @@ from . import serializers
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet): class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(Circuit, ['status']), (serializers.CircuitSerializer, ['status']),
(CircuitTermination, ['term_side']), (serializers.CircuitTerminationSerializer, ['term_side']),
) )
@ -32,7 +32,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
circuit_count=Count('circuits') circuit_count=Count('circuits')
) )
serializer_class = serializers.ProviderSerializer serializer_class = serializers.ProviderSerializer
filterset_class = filters.ProviderFilter filterset_class = filters.ProviderFilterSet
@action(detail=True) @action(detail=True)
def graphs(self, request, pk): def graphs(self, request, pk):
@ -40,7 +40,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
A convenience method for rendering graphs for a particular provider. A convenience method for rendering graphs for a particular provider.
""" """
provider = get_object_or_404(Provider, pk=pk) provider = get_object_or_404(Provider, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER) queryset = Graph.objects.filter(type__model='provider')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
return Response(serializer.data) return Response(serializer.data)
@ -54,7 +54,7 @@ class CircuitTypeViewSet(ModelViewSet):
circuit_count=Count('circuits') circuit_count=Count('circuits')
) )
serializer_class = serializers.CircuitTypeSerializer serializer_class = serializers.CircuitTypeSerializer
filterset_class = filters.CircuitTypeFilter filterset_class = filters.CircuitTypeFilterSet
# #
@ -62,9 +62,11 @@ class CircuitTypeViewSet(ModelViewSet):
# #
class CircuitViewSet(CustomFieldModelViewSet): class CircuitViewSet(CustomFieldModelViewSet):
queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags') queryset = Circuit.objects.prefetch_related(
'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint__device'
).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer serializer_class = serializers.CircuitSerializer
filterset_class = filters.CircuitFilter filterset_class = filters.CircuitFilterSet
# #
@ -76,4 +78,4 @@ class CircuitTerminationViewSet(ModelViewSet):
'circuit', 'site', 'connected_endpoint__device', 'cable' 'circuit', 'site', 'connected_endpoint__device', 'cable'
) )
serializer_class = serializers.CircuitTerminationSerializer serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filters.CircuitTerminationFilter filterset_class = filters.CircuitTerminationFilterSet

View File

@ -0,0 +1,48 @@
from utilities.choices import ChoiceSet
#
# Circuits
#
class CircuitStatusChoices(ChoiceSet):
STATUS_DEPROVISIONING = 'deprovisioning'
STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned'
STATUS_PROVISIONING = 'provisioning'
STATUS_OFFLINE = 'offline'
STATUS_DECOMMISSIONED = 'decommissioned'
CHOICES = (
(STATUS_PLANNED, 'Planned'),
(STATUS_PROVISIONING, 'Provisioning'),
(STATUS_ACTIVE, 'Active'),
(STATUS_OFFLINE, 'Offline'),
(STATUS_DEPROVISIONING, 'Deprovisioning'),
(STATUS_DECOMMISSIONED, 'Decommissioned'),
)
LEGACY_MAP = {
STATUS_DEPROVISIONING: 0,
STATUS_ACTIVE: 1,
STATUS_PLANNED: 2,
STATUS_PROVISIONING: 3,
STATUS_OFFLINE: 4,
STATUS_DECOMMISSIONED: 5,
}
#
# CircuitTerminations
#
class CircuitTerminationSideChoices(ChoiceSet):
SIDE_A = 'A'
SIDE_Z = 'Z'
CHOICES = (
(SIDE_A, 'A'),
(SIDE_Z, 'Z')
)

View File

@ -1,23 +0,0 @@
# Circuit statuses
CIRCUIT_STATUS_DEPROVISIONING = 0
CIRCUIT_STATUS_ACTIVE = 1
CIRCUIT_STATUS_PLANNED = 2
CIRCUIT_STATUS_PROVISIONING = 3
CIRCUIT_STATUS_OFFLINE = 4
CIRCUIT_STATUS_DECOMMISSIONED = 5
CIRCUIT_STATUS_CHOICES = [
[CIRCUIT_STATUS_PLANNED, 'Planned'],
[CIRCUIT_STATUS_PROVISIONING, 'Provisioning'],
[CIRCUIT_STATUS_ACTIVE, 'Active'],
[CIRCUIT_STATUS_OFFLINE, 'Offline'],
[CIRCUIT_STATUS_DEPROVISIONING, 'Deprovisioning'],
[CIRCUIT_STATUS_DECOMMISSIONED, 'Decommissioned'],
]
# CircuitTermination sides
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)

View File

@ -3,13 +3,20 @@ from django.db.models import Q
from dcim.models import Region, Site from dcim.models import Region, Site
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filters import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
from .constants import * from .choices import *
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
__all__ = (
'CircuitFilterSet',
'CircuitTerminationFilterSet',
'CircuitTypeFilterSet',
'ProviderFilterSet',
)
class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -58,14 +65,14 @@ class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
) )
class CircuitTypeFilter(NameSlugSearchFilterSet): class CircuitTypeFilterSet(NameSlugSearchFilterSet):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -95,7 +102,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter
label='Circuit type (slug)', label='Circuit type (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=CIRCUIT_STATUS_CHOICES, choices=CircuitStatusChoices,
null_value=None null_value=None
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
@ -139,7 +146,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter
).distinct() ).distinct()
class CircuitTerminationFilter(django_filters.FilterSet): class CircuitTerminationFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

View File

@ -1,26 +0,0 @@
[
{
"model": "circuits.circuittype",
"pk": 1,
"fields": {
"name": "Internet",
"slug": "internet"
}
},
{
"model": "circuits.circuittype",
"pk": 2,
"fields": {
"name": "Private WAN",
"slug": "private-wan"
}
},
{
"model": "circuits.circuittype",
"pk": 3,
"fields": {
"name": "Out-of-Band",
"slug": "out-of-band"
}
}
]

View File

@ -2,14 +2,17 @@ from django import forms
from taggit.forms import TagField from taggit.forms import TagField
from dcim.models import Region, Site from dcim.models import Region, Site
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
)
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, APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker,
DatePicker, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
StaticSelect2Multiple, TagFilterField,
) )
from .constants import * from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -17,7 +20,7 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
# Providers # Providers
# #
class ProviderForm(BootstrapMixin, CustomFieldForm): class ProviderForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
comments = CommentField() comments = CommentField()
tags = TagField( tags = TagField(
@ -46,7 +49,7 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
} }
class ProviderCSVForm(forms.ModelForm): class ProviderCSVForm(CustomFieldModelCSVForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -89,7 +92,8 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdi
label='Admin contact' label='Admin contact'
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea() widget=SmallTextarea,
label='Comments'
) )
class Meta: class Meta:
@ -104,7 +108,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False, required=False,
label='Search' label='Search'
) )
region = FilterChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
@ -116,9 +120,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
} }
) )
) )
site = FilterChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
@ -128,6 +133,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False, required=False,
label='ASN' label='ASN'
) )
tag = TagFilterField(model)
# #
@ -140,7 +146,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = [ fields = [
'name', 'slug', 'name', 'slug', 'description',
] ]
@ -159,7 +165,19 @@ class CircuitTypeCSVForm(forms.ModelForm):
# Circuits # Circuits
# #
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
widget=APISelect(
api_url="/api/circuits/providers/"
)
)
type = DynamicModelChoiceField(
queryset=CircuitType.objects.all(),
widget=APISelect(
api_url="/api/circuits/circuit-types/"
)
)
comments = CommentField() comments = CommentField()
tags = TagField( tags = TagField(
required=False required=False
@ -176,18 +194,12 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'commit_rate': "Committed rate", 'commit_rate': "Committed rate",
} }
widgets = { widgets = {
'provider': APISelect(
api_url="/api/circuits/providers/"
),
'type': APISelect(
api_url="/api/circuits/circuit-types/"
),
'status': StaticSelect2(), 'status': StaticSelect2(),
'install_date': DatePicker(), 'install_date': DatePicker(),
} }
class CircuitCSVForm(forms.ModelForm): class CircuitCSVForm(CustomFieldModelCSVForm):
provider = forms.ModelChoiceField( provider = forms.ModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='name', to_field_name='name',
@ -205,7 +217,7 @@ class CircuitCSVForm(forms.ModelForm):
} }
) )
status = CSVChoiceField( status = CSVChoiceField(
choices=CIRCUIT_STATUS_CHOICES, choices=CircuitStatusChoices,
required=False, required=False,
help_text='Operational status' help_text='Operational status'
) )
@ -231,14 +243,14 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
queryset=Circuit.objects.all(), queryset=Circuit.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
type = forms.ModelChoiceField( type = DynamicModelChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url="/api/circuits/circuit-types/" api_url="/api/circuits/circuit-types/"
) )
) )
provider = forms.ModelChoiceField( provider = DynamicModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -246,12 +258,12 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
) )
) )
status = forms.ChoiceField( status = forms.ChoiceField(
choices=add_blank_choice(CIRCUIT_STATUS_CHOICES), choices=add_blank_choice(CircuitStatusChoices),
required=False, required=False,
initial='', initial='',
widget=StaticSelect2() widget=StaticSelect2()
) )
tenant = forms.ModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -286,28 +298,30 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
required=False, required=False,
label='Search' label='Search'
) )
type = FilterChoiceField( type = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/circuits/circuit-types/", api_url="/api/circuits/circuit-types/",
value_field="slug", value_field="slug",
) )
) )
provider = FilterChoiceField( provider = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/circuits/providers/", api_url="/api/circuits/providers/",
value_field="slug", value_field="slug",
) )
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=CIRCUIT_STATUS_CHOICES, choices=CircuitStatusChoices,
required=False, required=False,
widget=StaticSelect2Multiple() widget=StaticSelect2Multiple()
) )
region = forms.ModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
@ -319,9 +333,10 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
} }
) )
) )
site = FilterChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
@ -332,6 +347,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
min_value=0, min_value=0,
label='Commit rate (Kbps)' label='Commit rate (Kbps)'
) )
tag = TagFilterField(model)
# #

View File

@ -1,40 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:25
import dcim.fields
from django.db import migrations, models
import django.db.models.deletion 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): 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'), ('circuits', '0007_circuit_add_description'), ('circuits', '0008_circuittermination_interface_protect_on_delete'), ('circuits', '0009_unicode_literals'), ('circuits', '0010_circuit_status')] 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 = [ dependencies = [
('tenancy', '0001_initial'),
('dcim', '0001_initial'), ('dcim', '0001_initial'),
('dcim', '0022_color_names_to_rgb'), ('dcim', '0022_color_names_to_rgb'),
('tenancy', '0001_initial'),
] ]
operations = [ operations = [
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='ASN')),
('account', models.CharField(blank=True, max_length=30, verbose_name='Account number')),
('portal_url', models.URLField(blank=True, verbose_name='Portal')),
('noc_contact', models.TextField(blank=True, verbose_name='NOC contact')),
('admin_contact', models.TextField(blank=True, verbose_name='Admin contact')),
('comments', models.TextField(blank=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='CircuitType', name='CircuitType',
fields=[ fields=[
@ -46,49 +42,93 @@ class Migration(migrations.Migration):
'ordering': ['name'], '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( migrations.CreateModel(
name='Circuit', name='Circuit',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)), ('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)), ('last_updated', models.DateTimeField(auto_now=True)),
('cid', models.CharField(max_length=50, verbose_name='Circuit ID')), ('cid', models.CharField(max_length=50, verbose_name=b'Circuit ID')),
('install_date', models.DateField(blank=True, null=True, verbose_name='Date installed')), ('install_date', models.DateField(blank=True, null=True, verbose_name=b'Date installed')),
('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')), ('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)), ('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')), ('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')), ('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')), ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant')),
('description', models.CharField(blank=True, max_length=100)), ('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')),
('status', models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1))
], ],
options={ options={
'ordering': ['provider', 'cid'], 'ordering': ['provider', 'cid'],
'unique_together': {('provider', 'cid')},
}, },
), ),
migrations.AlterUniqueTogether(
name='circuit',
unique_together=set([('provider', 'cid')]),
),
migrations.CreateModel( migrations.CreateModel(
name='CircuitTermination', name='CircuitTermination',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('term_side', models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination')), ('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='Port speed (Kbps)')), ('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')),
('upstream_speed', models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream 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='Cross-connect ID')), ('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='Patch panel/port(s)')), ('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')), ('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.PROTECT, related_name='circuit_termination', to='dcim.Interface')), ('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')), ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.Site')),
], ],
options={ options={
'ordering': ['circuit', 'term_side'], 'ordering': ['circuit', 'term_side'],
'unique_together': {('circuit', 'term_side')},
}, },
), ),
migrations.AlterUniqueTogether( migrations.RunPython(
name='circuittermination', code=circuits_to_terms,
unique_together=set([('circuit', 'term_side')]), ),
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

@ -0,0 +1,254 @@
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

@ -3,7 +3,7 @@ import sys
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from dcim.constants import CONNECTION_STATUS_CONNECTED CONNECTION_STATUS_CONNECTED = True
def circuit_terminations_to_cables(apps, schema_editor): def circuit_terminations_to_cables(apps, schema_editor):

View File

@ -0,0 +1,39 @@
from django.db import migrations, models
CIRCUIT_STATUS_CHOICES = (
(0, 'deprovisioning'),
(1, 'active'),
(2, 'planned'),
(3, 'provisioning'),
(4, 'offline'),
(5, 'decommissioned')
)
def circuit_status_to_slug(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit')
for id, slug in CIRCUIT_STATUS_CHOICES:
Circuit.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('circuits', '0015_custom_tag_models'),
]
operations = [
# Circuit.status
migrations.AlterField(
model_name='circuit',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=circuit_status_to_slug
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.6 on 2019-12-10 18:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0016_3569_circuit_fields'),
]
operations = [
migrations.AddField(
model_name='circuittype',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@ -3,13 +3,21 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES 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 utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object from utilities.utils import serialize_object
from .constants import * from .choices import *
__all__ = (
'Circuit',
'CircuitTermination',
'CircuitType',
'Provider',
)
class Provider(ChangeLoggedModel, CustomFieldModel): class Provider(ChangeLoggedModel, CustomFieldModel):
@ -57,7 +65,12 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] csv_headers = [
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
]
clone_fields = [
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
]
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -93,8 +106,12 @@ class CircuitType(ChangeLoggedModel):
slug = models.SlugField( slug = models.SlugField(
unique=True unique=True
) )
description = models.CharField(
max_length=100,
blank=True,
)
csv_headers = ['name', 'slug'] csv_headers = ['name', 'slug', 'description']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -109,6 +126,7 @@ class CircuitType(ChangeLoggedModel):
return ( return (
self.name, self.name,
self.slug, self.slug,
self.description,
) )
@ -132,9 +150,10 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='circuits' related_name='circuits'
) )
status = models.PositiveSmallIntegerField( status = models.CharField(
choices=CIRCUIT_STATUS_CHOICES, max_length=50,
default=CIRCUIT_STATUS_ACTIVE choices=CircuitStatusChoices,
default=CircuitStatusChoices.STATUS_ACTIVE
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
@ -170,6 +189,18 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
csv_headers = [ csv_headers = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
] ]
clone_fields = [
'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
]
STATUS_CLASS_MAP = {
CircuitStatusChoices.STATUS_DEPROVISIONING: 'warning',
CircuitStatusChoices.STATUS_ACTIVE: 'success',
CircuitStatusChoices.STATUS_PLANNED: 'info',
CircuitStatusChoices.STATUS_PROVISIONING: 'primary',
CircuitStatusChoices.STATUS_OFFLINE: 'danger',
CircuitStatusChoices.STATUS_DECOMMISSIONED: 'default',
}
class Meta: class Meta:
ordering = ['provider', 'cid'] ordering = ['provider', 'cid']
@ -195,7 +226,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
) )
def get_status_class(self): def get_status_class(self):
return STATUS_CLASSES[self.status] return self.STATUS_CLASS_MAP.get(self.status)
def _get_termination(self, side): def _get_termination(self, side):
for ct in self.terminations.all(): for ct in self.terminations.all():
@ -220,7 +251,7 @@ class CircuitTermination(CableTermination):
) )
term_side = models.CharField( term_side = models.CharField(
max_length=1, max_length=1,
choices=TERM_SIDE_CHOICES, choices=CircuitTerminationSideChoices,
verbose_name='Termination' verbose_name='Termination'
) )
site = models.ForeignKey( site = models.ForeignKey(

View File

@ -6,7 +6,7 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_ACTIONS = """ CIRCUITTYPE_ACTIONS = """
<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog"> <a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.circuit.change_circuittype %} {% if perms.circuit.change_circuittype %}
@ -50,12 +50,14 @@ class CircuitTypeTable(BaseTable):
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, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' template_code=CIRCUITTYPE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CircuitType model = CircuitType
fields = ('pk', 'name', 'circuit_count', 'slug', 'actions') fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
# #

View File

@ -1,12 +1,35 @@
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z from circuits.choices import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site from dcim.models import Site
from extras.constants import GRAPH_TYPE_PROVIDER
from extras.models import Graph from extras.models import Graph
from utilities.testing import APITestCase from utilities.testing import APITestCase, choices_to_dict
class AppTest(APITestCase):
def test_root(self):
url = reverse('circuits-api:api-root')
response = self.client.get('{}?format=api'.format(url), **self.header)
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):
@ -28,16 +51,20 @@ class ProviderTest(APITestCase):
def test_get_provider_graphs(self): def test_get_provider_graphs(self):
provider_ct = ContentType.objects.get(app_label='circuits', model='provider')
self.graph1 = Graph.objects.create( self.graph1 = Graph.objects.create(
type=GRAPH_TYPE_PROVIDER, name='Test Graph 1', type=provider_ct,
name='Test Graph 1',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1' source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'
) )
self.graph2 = Graph.objects.create( self.graph2 = Graph.objects.create(
type=GRAPH_TYPE_PROVIDER, name='Test Graph 2', type=provider_ct,
name='Test Graph 2',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2' source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'
) )
self.graph3 = Graph.objects.create( self.graph3 = Graph.objects.create(
type=GRAPH_TYPE_PROVIDER, name='Test Graph 3', type=provider_ct,
name='Test Graph 3',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3' source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'
) )
@ -250,7 +277,7 @@ class CircuitTest(APITestCase):
'cid': 'TEST0004', 'cid': 'TEST0004',
'provider': self.provider1.pk, 'provider': self.provider1.pk,
'type': self.circuittype1.pk, 'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE, 'status': CircuitStatusChoices.STATUS_ACTIVE,
} }
url = reverse('circuits-api:circuit-list') url = reverse('circuits-api:circuit-list')
@ -270,19 +297,19 @@ class CircuitTest(APITestCase):
'cid': 'TEST0004', 'cid': 'TEST0004',
'provider': self.provider1.pk, 'provider': self.provider1.pk,
'type': self.circuittype1.pk, 'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE, 'status': CircuitStatusChoices.STATUS_ACTIVE,
}, },
{ {
'cid': 'TEST0005', 'cid': 'TEST0005',
'provider': self.provider1.pk, 'provider': self.provider1.pk,
'type': self.circuittype1.pk, 'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE, 'status': CircuitStatusChoices.STATUS_ACTIVE,
}, },
{ {
'cid': 'TEST0006', 'cid': 'TEST0006',
'provider': self.provider1.pk, 'provider': self.provider1.pk,
'type': self.circuittype1.pk, 'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE, 'status': CircuitStatusChoices.STATUS_ACTIVE,
}, },
] ]
@ -336,16 +363,28 @@ class CircuitTerminationTest(APITestCase):
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype) self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype) self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
self.circuittermination1 = CircuitTermination.objects.create( self.circuittermination1 = CircuitTermination.objects.create(
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 circuit=self.circuit1,
term_side=CircuitTerminationSideChoices.SIDE_A,
site=self.site1,
port_speed=1000000
) )
self.circuittermination2 = CircuitTermination.objects.create( self.circuittermination2 = CircuitTermination.objects.create(
circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000 circuit=self.circuit1,
term_side=CircuitTerminationSideChoices.SIDE_Z,
site=self.site2,
port_speed=1000000
) )
self.circuittermination3 = CircuitTermination.objects.create( self.circuittermination3 = CircuitTermination.objects.create(
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 circuit=self.circuit2,
term_side=CircuitTerminationSideChoices.SIDE_A,
site=self.site1,
port_speed=1000000
) )
self.circuittermination4 = CircuitTermination.objects.create( self.circuittermination4 = CircuitTermination.objects.create(
circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000 circuit=self.circuit2,
term_side=CircuitTerminationSideChoices.SIDE_Z,
site=self.site2,
port_speed=1000000
) )
def test_get_circuittermination(self): def test_get_circuittermination(self):
@ -366,7 +405,7 @@ class CircuitTerminationTest(APITestCase):
data = { data = {
'circuit': self.circuit3.pk, 'circuit': self.circuit3.pk,
'term_side': TERM_SIDE_A, 'term_side': CircuitTerminationSideChoices.SIDE_A,
'site': self.site1.pk, 'site': self.site1.pk,
'port_speed': 1000000, 'port_speed': 1000000,
} }
@ -385,12 +424,15 @@ class CircuitTerminationTest(APITestCase):
def test_update_circuittermination(self): def test_update_circuittermination(self):
circuittermination5 = CircuitTermination.objects.create( circuittermination5 = CircuitTermination.objects.create(
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 circuit=self.circuit3,
term_side=CircuitTerminationSideChoices.SIDE_A,
site=self.site1,
port_speed=1000000
) )
data = { data = {
'circuit': self.circuit3.pk, 'circuit': self.circuit3.pk,
'term_side': TERM_SIDE_Z, 'term_side': CircuitTerminationSideChoices.SIDE_Z,
'site': self.site2.pk, 'site': self.site2.pk,
'port_speed': 1000000, 'port_speed': 1000000,
} }

View File

@ -0,0 +1,287 @@
from django.test import TestCase
from circuits.choices import *
from circuits.filters import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Region, Site
class ProviderTestCase(TestCase):
queryset = Provider.objects.all()
filterset = ProviderFilterSet
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'),
Provider(name='Provider 2', slug='provider-2', asn=65002, account='2345'),
Provider(name='Provider 3', slug='provider-3', asn=65003, account='3456'),
Provider(name='Provider 4', slug='provider-4', asn=65004, account='4567'),
Provider(name='Provider 5', slug='provider-5', asn=65005, account='5678'),
)
Provider.objects.bulk_create(providers)
regions = (
Region(name='Test Region 1', slug='test-region-1'),
Region(name='Test Region 2', slug='test-region-2'),
)
# Can't use bulk_create for models with MPTT fields
for r in regions:
r.save()
sites = (
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
)
Site.objects.bulk_create(sites)
circuit_types = (
CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'),
CircuitType(name='Test Circuit Type 2', slug='test-circuit-type-2'),
)
CircuitType.objects.bulk_create(circuit_types)
circuits = (
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'),
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 1'),
)
Circuit.objects.bulk_create(circuits)
CircuitTermination.objects.bulk_create((
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000),
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A', port_speed=1000),
))
def test_name(self):
params = {'name': ['Provider 1', 'Provider 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_slug(self):
params = {'slug': ['provider-1', 'provider-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_asn(self):
params = {'asn': ['65001', '65002']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_account(self):
params = {'account': ['1234', '2345']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_id__in(self):
id_list = self.queryset.values_list('id', flat=True)[:3]
params = {'id__in': ','.join([str(id) for id in id_list])}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CircuitTypeTestCase(TestCase):
queryset = CircuitType.objects.all()
filterset = CircuitTypeFilterSet
@classmethod
def setUpTestData(cls):
CircuitType.objects.bulk_create((
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
))
def test_id(self):
params = {'id': [self.queryset.first().pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_name(self):
params = {'name': ['Circuit Type 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_slug(self):
params = {'slug': ['circuit-type-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class CircuitTestCase(TestCase):
queryset = Circuit.objects.all()
filterset = CircuitFilterSet
@classmethod
def setUpTestData(cls):
regions = (
Region(name='Test Region 1', slug='test-region-1'),
Region(name='Test Region 2', slug='test-region-2'),
Region(name='Test Region 3', slug='test-region-3'),
)
# Can't use bulk_create for models with MPTT fields
for r in regions:
r.save()
sites = (
Site(name='Test Site 1', slug='test-site-1', region=regions[0]),
Site(name='Test Site 2', slug='test-site-2', region=regions[1]),
Site(name='Test Site 3', slug='test-site-3', region=regions[2]),
)
Site.objects.bulk_create(sites)
circuit_types = (
CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'),
CircuitType(name='Test Circuit Type 2', slug='test-circuit-type-2'),
)
CircuitType.objects.bulk_create(circuit_types)
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
)
Provider.objects.bulk_create(providers)
circuits = (
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE),
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE),
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
)
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=1000),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=1000),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
def test_cid(self):
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_install_date(self):
params = {'install_date': ['2020-01-01', '2020-01-02']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_commit_rate(self):
params = {'commit_rate': ['1000', '2000']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_id__in(self):
id_list = self.queryset.values_list('id', flat=True)[:3]
params = {'id__in': ','.join([str(id) for id in id_list])}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_provider(self):
provider = Provider.objects.first()
params = {'provider_id': [provider.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'provider': [provider.slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_type(self):
circuit_type = CircuitType.objects.first()
params = {'type_id': [circuit_type.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'type': [circuit_type.slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_status(self):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CircuitTerminationTestCase(TestCase):
queryset = CircuitTermination.objects.all()
filterset = CircuitTerminationFilterSet
@classmethod
def setUpTestData(cls):
sites = (
Site(name='Test Site 1', slug='test-site-1'),
Site(name='Test Site 2', slug='test-site-2'),
Site(name='Test Site 3', slug='test-site-3'),
)
Site.objects.bulk_create(sites)
circuit_types = (
CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'),
)
CircuitType.objects.bulk_create(circuit_types)
providers = (
Provider(name='Provider 1', slug='provider-1'),
)
Provider.objects.bulk_create(providers)
circuits = (
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 2'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 3'),
)
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC'),
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
def test_term_side(self):
params = {'term_side': 'A'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_port_speed(self):
params = {'port_speed': ['1000', '2000']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_upstream_speed(self):
params = {'upstream_speed': ['1000', '2000']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_xconnect_id(self):
params = {'xconnect_id': ['ABC', 'DEF']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_circuit_id(self):
circuits = Circuit.objects.all()[:2]
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)

View File

@ -1,18 +1,15 @@
import urllib.parse import datetime
from django.test import Client, TestCase
from django.urls import reverse
from circuits.choices import *
from circuits.models import Circuit, CircuitType, Provider from circuits.models import Circuit, CircuitType, Provider
from utilities.testing import create_test_user from utilities.testing import ViewTestCases
class ProviderTestCase(TestCase): class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Provider
def setUp(self): @classmethod
user = create_test_user(permissions=['circuits.view_provider']) def setUpTestData(cls):
self.client = Client()
self.client.force_login(user)
Provider.objects.bulk_create([ Provider.objects.bulk_create([
Provider(name='Provider 1', slug='provider-1', asn=65001), Provider(name='Provider 1', slug='provider-1', asn=65001),
@ -20,29 +17,40 @@ class ProviderTestCase(TestCase):
Provider(name='Provider 3', slug='provider-3', asn=65003), Provider(name='Provider 3', slug='provider-3', asn=65003),
]) ])
def test_provider_list(self): cls.form_data = {
'name': 'Provider X',
url = reverse('circuits:provider_list') 'slug': 'provider-x',
params = { 'asn': 65123,
"q": "test", 'account': '1234',
'portal_url': 'http://example.com/portal',
'noc_contact': 'noc@example.com',
'admin_contact': 'admin@example.com',
'comments': 'Another provider',
'tags': 'Alpha,Bravo,Charlie',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.csv_data = (
self.assertEqual(response.status_code, 200) "name,slug",
"Provider 4,provider-4",
"Provider 5,provider-5",
"Provider 6,provider-6",
)
def test_provider(self): cls.bulk_edit_data = {
'asn': 65009,
provider = Provider.objects.first() 'account': '5678',
response = self.client.get(provider.get_absolute_url()) 'portal_url': 'http://example.com/portal2',
self.assertEqual(response.status_code, 200) 'noc_contact': 'noc2@example.com',
'admin_contact': 'admin2@example.com',
'comments': 'New comments',
}
class CircuitTypeTestCase(TestCase): class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = CircuitType
def setUp(self): @classmethod
user = create_test_user(permissions=['circuits.view_circuittype']) def setUpTestData(cls):
self.client = Client()
self.client.force_login(user)
CircuitType.objects.bulk_create([ CircuitType.objects.bulk_create([
CircuitType(name='Circuit Type 1', slug='circuit-type-1'), CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
@ -50,46 +58,71 @@ class CircuitTypeTestCase(TestCase):
CircuitType(name='Circuit Type 3', slug='circuit-type-3'), CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
]) ])
def test_circuittype_list(self): cls.form_data = {
'name': 'Circuit Type X',
url = reverse('circuits:circuittype_list') 'slug': 'circuit-type-x',
'description': 'A new circuit type',
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class CircuitTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['circuits.view_circuit'])
self.client = Client()
self.client.force_login(user)
provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
provider.save()
circuittype = CircuitType(name='Circuit Type 1', slug='circuit-type-1')
circuittype.save()
Circuit.objects.bulk_create([
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
])
def test_circuit_list(self):
url = reverse('circuits:circuit_list')
params = {
"provider": Provider.objects.first().slug,
"type": CircuitType.objects.first().slug,
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.csv_data = (
self.assertEqual(response.status_code, 200) "name,slug",
"Circuit Type 4,circuit-type-4",
"Circuit Type 5,circuit-type-5",
"Circuit Type 6,circuit-type-6",
)
def test_circuit(self):
circuit = Circuit.objects.first() class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
response = self.client.get(circuit.get_absolute_url()) model = Circuit
self.assertEqual(response.status_code, 200)
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1', asn=65001),
Provider(name='Provider 2', slug='provider-2', asn=65002),
)
Provider.objects.bulk_create(providers)
circuittypes = (
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
)
CircuitType.objects.bulk_create(circuittypes)
Circuit.objects.bulk_create([
Circuit(cid='Circuit 1', provider=providers[0], type=circuittypes[0]),
Circuit(cid='Circuit 2', provider=providers[0], type=circuittypes[0]),
Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
])
cls.form_data = {
'cid': 'Circuit X',
'provider': providers[1].pk,
'type': circuittypes[1].pk,
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
'tenant': None,
'install_date': datetime.date(2020, 1, 1),
'commit_rate': 1000,
'description': 'A new circuit',
'comments': 'Some comments',
'tags': 'Alpha,Bravo,Charlie',
}
cls.csv_data = (
"cid,provider,type",
"Circuit 4,Provider 1,Circuit Type 1",
"Circuit 5,Provider 1,Circuit Type 1",
"Circuit 6,Provider 1,Circuit Type 1",
)
cls.bulk_edit_data = {
'provider': providers[1].pk,
'type': circuittypes[1].pk,
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
'tenant': None,
'commit_rate': 2000,
'description': 'New description',
'comments': 'New comments',
}

View File

@ -9,42 +9,42 @@ app_name = 'circuits'
urlpatterns = [ urlpatterns = [
# Providers # Providers
path(r'providers/', views.ProviderListView.as_view(), name='provider_list'), path('providers/', views.ProviderListView.as_view(), name='provider_list'),
path(r'providers/add/', views.ProviderCreateView.as_view(), name='provider_add'), path('providers/add/', views.ProviderCreateView.as_view(), name='provider_add'),
path(r'providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'), path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
path(r'providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
path(r'providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
path(r'providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'), path('providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'),
path(r'providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'), path('providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
path(r'providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), path('providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
path(r'providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), path('providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
# Circuit types # Circuit types
path(r'circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
path(r'circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), path('circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
path(r'circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
path(r'circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
path(r'circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), path('circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
path(r'circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), path('circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
# Circuits # Circuits
path(r'circuits/', views.CircuitListView.as_view(), name='circuit_list'), path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
path(r'circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'), path('circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'),
path(r'circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'), path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
path(r'circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
path(r'circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
path(r'circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'), path('circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'),
path(r'circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'), path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
path(r'circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
path(r'circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
path(r'circuits/<int:pk>/terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'), path('circuits/<int:pk>/terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'),
# Circuit terminations # Circuit terminations
path(r'circuits/<int:circuit>/terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), path('circuits/<int:circuit>/terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
path(r'circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
path(r'circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
path(r'circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), path('circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
path(r'circuit-terminations/<int:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), path('circuit-terminations/<int:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
] ]

View File

@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
@ -5,14 +6,16 @@ from django.db import transaction
from django.db.models import Count, OuterRef, Subquery from django.db.models import Count, OuterRef, Subquery
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import View from django.views.generic import View
from django_tables2 import RequestConfig
from extras.models import Graph, GRAPH_TYPE_PROVIDER from extras.models import Graph
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .constants import TERM_SIDE_A, TERM_SIDE_Z from .choices import CircuitTerminationSideChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -23,10 +26,9 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderListView(PermissionRequiredMixin, ObjectListView): class ProviderListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'circuits.view_provider' permission_required = 'circuits.view_provider'
queryset = Provider.objects.annotate(count_circuits=Count('circuits')) queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filter = filters.ProviderFilter filterset = filters.ProviderFilterSet
filter_form = forms.ProviderFilterForm filterset_form = forms.ProviderFilterForm
table = tables.ProviderDetailTable table = tables.ProviderDetailTable
template_name = 'circuits/provider_list.html'
class ProviderView(PermissionRequiredMixin, View): class ProviderView(PermissionRequiredMixin, View):
@ -36,11 +38,20 @@ class ProviderView(PermissionRequiredMixin, View):
provider = get_object_or_404(Provider, slug=slug) provider = get_object_or_404(Provider, slug=slug)
circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site') circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists() show_graphs = Graph.objects.filter(type__model='provider').exists()
circuits_table = tables.CircuitTable(circuits, orderable=False)
circuits_table.columns.hide('provider')
paginate = {
'paginator_class': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
}
RequestConfig(request, paginate).configure(circuits_table)
return render(request, 'circuits/provider.html', { return render(request, 'circuits/provider.html', {
'provider': provider, 'provider': provider,
'circuits': circuits, 'circuits_table': circuits_table,
'show_graphs': show_graphs, 'show_graphs': show_graphs,
}) })
@ -73,7 +84,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.all()
filter = filters.ProviderFilter filterset = filters.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
form = forms.ProviderBulkEditForm form = forms.ProviderBulkEditForm
default_return_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
@ -82,7 +93,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.all()
filter = filters.ProviderFilter filterset = filters.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
default_return_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
@ -95,7 +106,6 @@ class CircuitTypeListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'circuits.view_circuittype' permission_required = 'circuits.view_circuittype'
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')) queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
template_name = 'circuits/circuittype_list.html'
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView): class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
@ -136,10 +146,9 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView):
a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]), a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]),
z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]), z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]),
) )
filter = filters.CircuitFilter filterset = filters.CircuitFilterSet
filter_form = forms.CircuitFilterForm filterset_form = forms.CircuitFilterForm
table = tables.CircuitTable table = tables.CircuitTable
template_name = 'circuits/circuit_list.html'
class CircuitView(PermissionRequiredMixin, View): class CircuitView(PermissionRequiredMixin, View):
@ -151,12 +160,12 @@ class CircuitView(PermissionRequiredMixin, View):
termination_a = CircuitTermination.objects.prefetch_related( termination_a = CircuitTermination.objects.prefetch_related(
'site__region', 'connected_endpoint__device' 'site__region', 'connected_endpoint__device'
).filter( ).filter(
circuit=circuit, term_side=TERM_SIDE_A circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
).first() ).first()
termination_z = CircuitTermination.objects.prefetch_related( termination_z = CircuitTermination.objects.prefetch_related(
'site__region', 'connected_endpoint__device' 'site__region', 'connected_endpoint__device'
).filter( ).filter(
circuit=circuit, term_side=TERM_SIDE_Z circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first() ).first()
return render(request, 'circuits/circuit.html', { return render(request, 'circuits/circuit.html', {
@ -194,7 +203,7 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_circuit' permission_required = 'circuits.change_circuit'
queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site') queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter filterset = filters.CircuitFilterSet
table = tables.CircuitTable table = tables.CircuitTable
form = forms.CircuitBulkEditForm form = forms.CircuitBulkEditForm
default_return_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
@ -203,7 +212,7 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit' permission_required = 'circuits.delete_circuit'
queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site') queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter filterset = filters.CircuitFilterSet
table = tables.CircuitTable table = tables.CircuitTable
default_return_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
@ -212,8 +221,12 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
def circuit_terminations_swap(request, pk): def circuit_terminations_swap(request, pk):
circuit = get_object_or_404(Circuit, pk=pk) circuit = get_object_or_404(Circuit, pk=pk)
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first() termination_a = CircuitTermination.objects.filter(
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first() circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
).first()
termination_z = CircuitTermination.objects.filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
if not termination_a and not termination_z: if not termination_a and not termination_z:
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit)) messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
return redirect('circuits:circuit', pk=circuit.pk) return redirect('circuits:circuit', pk=circuit.pk)

View File

@ -1,10 +1,11 @@
from rest_framework import serializers from rest_framework import serializers
from dcim.constants import CONNECTION_STATUS_CHOICES, IFACE_TYPE_CHOICES from dcim.choices import InterfaceTypeChoices
from dcim.constants import CONNECTION_STATUS_CHOICES
from dcim.models import ( from dcim.models import (
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, Rack, RackGroup, RackRole, Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, PowerPortTemplate, Rack,
RearPort, RearPortTemplate, Region, Site, VirtualChassis, RackGroup, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
) )
from utilities.api import ChoiceField, WritableNestedSerializer from utilities.api import ChoiceField, WritableNestedSerializer
@ -25,6 +26,7 @@ __all__ = [
'NestedPowerOutletSerializer', 'NestedPowerOutletSerializer',
'NestedPowerPanelSerializer', 'NestedPowerPanelSerializer',
'NestedPowerPortSerializer', 'NestedPowerPortSerializer',
'NestedPowerPortTemplateSerializer',
'NestedRackGroupSerializer', 'NestedRackGroupSerializer',
'NestedRackRoleSerializer', 'NestedRackRoleSerializer',
'NestedRackSerializer', 'NestedRackSerializer',
@ -111,6 +113,14 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count'] fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
class Meta:
model = PowerPortTemplate
fields = ['id', 'url', 'name']
class NestedRearPortTemplateSerializer(WritableNestedSerializer): class NestedRearPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
@ -203,7 +213,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) type = ChoiceField(choices=InterfaceTypeChoices)
class Meta: class Meta:
model = Interface model = Interface

View File

@ -4,6 +4,7 @@ from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import ( from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@ -67,7 +68,7 @@ class RegionSerializer(CustomFieldModelSerializer):
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer): class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False) status = ChoiceField(choices=SiteStatusChoices, required=False)
region = NestedRegionSerializer(required=False, allow_null=True) region = NestedRegionSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False) time_zone = TimeZoneField(required=False)
@ -107,18 +108,18 @@ class RackRoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'name', 'slug', 'color', 'rack_count'] fields = ['id', 'name', 'slug', 'color', 'description', 'rack_count']
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
site = NestedSiteSerializer() site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None) group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False) status = ChoiceField(choices=RackStatusChoices, required=False)
role = NestedRackRoleSerializer(required=False, allow_null=True) role = NestedRackRoleSerializer(required=False, allow_null=True)
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True) type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False) width = ChoiceField(choices=RackWidthChoices, required=False)
outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False) outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
powerfeed_count = serializers.IntegerField(read_only=True) powerfeed_count = serializers.IntegerField(read_only=True)
@ -156,7 +157,7 @@ class RackUnitSerializer(serializers.Serializer):
""" """
id = serializers.IntegerField(read_only=True) id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True)
face = serializers.IntegerField(read_only=True) face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
@ -170,6 +171,42 @@ class RackReservationSerializer(ValidatedModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description'] fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
class RackElevationDetailFilterSerializer(serializers.Serializer):
q = serializers.CharField(
required=False,
default=None
)
face = serializers.ChoiceField(
choices=DeviceFaceChoices,
default=DeviceFaceChoices.FACE_FRONT
)
render = serializers.ChoiceField(
choices=RackElevationDetailRenderChoices,
default=RackElevationDetailRenderChoices.RENDER_JSON
)
unit_width = serializers.IntegerField(
default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT
)
unit_height = serializers.IntegerField(
default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
)
legend_width = serializers.IntegerField(
default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
)
exclude = serializers.IntegerField(
required=False,
default=None
)
expand_devices = serializers.BooleanField(
required=False,
default=True
)
include_images = serializers.BooleanField(
required=False,
default=True
)
# #
# Device types # Device types
# #
@ -186,7 +223,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
manufacturer = NestedManufacturerSerializer() manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True) subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
@ -194,64 +231,83 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
model = DeviceType model = DeviceType
fields = [ fields = [
'id', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth', 'id', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
'subdevice_role', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
'last_updated', 'device_count',
] ]
class ConsolePortTemplateSerializer(ValidatedModelSerializer): class ConsolePortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
required=False
)
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name', 'type']
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
required=False
)
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name', 'type']
class PowerPortTemplateSerializer(ValidatedModelSerializer): class PowerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=PowerPortTypeChoices,
allow_blank=True,
required=False
)
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw'] fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateSerializer(ValidatedModelSerializer): class PowerOutletTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
power_port = PowerPortTemplateSerializer( type = ChoiceField(
choices=PowerOutletTypeChoices,
allow_blank=True,
required=False
)
power_port = NestedPowerPortTemplateSerializer(
required=False required=False
) )
feed_leg = ChoiceField( feed_leg = ChoiceField(
choices=POWERFEED_LEG_CHOICES, choices=PowerOutletFeedLegChoices,
required=False, allow_blank=True,
allow_null=True required=False
) )
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg'] fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg']
class InterfaceTemplateSerializer(ValidatedModelSerializer): class InterfaceTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) type = ChoiceField(choices=InterfaceTypeChoices)
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'device_type', 'name', 'type', 'form_factor', 'mgmt_only'] fields = ['id', 'device_type', 'name', 'type', 'mgmt_only']
class RearPortTemplateSerializer(ValidatedModelSerializer): class RearPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES) type = ChoiceField(choices=PortTypeChoices)
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
@ -260,7 +316,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
class FrontPortTemplateSerializer(ValidatedModelSerializer): class FrontPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES) type = ChoiceField(choices=PortTypeChoices)
rear_port = NestedRearPortTemplateSerializer() rear_port = NestedRearPortTemplateSerializer()
class Meta: class Meta:
@ -286,7 +342,9 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role', 'device_count', 'virtualmachine_count'] fields = [
'id', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count',
]
class PlatformSerializer(ValidatedModelSerializer): class PlatformSerializer(ValidatedModelSerializer):
@ -309,8 +367,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
platform = NestedPlatformSerializer(required=False, allow_null=True) platform = NestedPlatformSerializer(required=False, allow_null=True)
site = NestedSiteSerializer() site = NestedSiteSerializer()
rack = NestedRackSerializer(required=False, allow_null=True) rack = NestedRackSerializer(required=False, allow_null=True)
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True) face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False) status = ChoiceField(choices=DeviceStatusChoices, required=False)
primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip = NestedIPAddressSerializer(read_only=True)
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
@ -370,41 +428,60 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
return obj.get_config_context() return obj.get_config_context()
class DeviceNAPALMSerializer(serializers.Serializer):
method = serializers.DictField()
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = [ fields = [
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
'cable', 'tags', 'connection_status', 'cable', 'tags',
] ]
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = [ fields = [
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
'cable', 'tags', 'connection_status', 'cable', 'tags',
] ]
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(
choices=PowerOutletTypeChoices,
allow_blank=True,
required=False
)
power_port = NestedPowerPortSerializer( power_port = NestedPowerPortSerializer(
required=False required=False
) )
feed_leg = ChoiceField( feed_leg = ChoiceField(
choices=POWERFEED_LEG_CHOICES, choices=PowerOutletFeedLegChoices,
required=False, allow_blank=True,
allow_null=True required=False
) )
cable = NestedCableSerializer( cable = NestedCableSerializer(
read_only=True read_only=True
@ -416,31 +493,34 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'device', 'name', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type', 'id', 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags', 'connected_endpoint', 'connection_status', 'cable', 'tags',
] ]
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(
choices=PowerPortTypeChoices,
allow_blank=True,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = [ fields = [
'id', 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type', 'id', 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags', 'connected_endpoint', 'connection_status', 'cable', 'tags',
] ]
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) type = ChoiceField(choices=InterfaceTypeChoices)
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
lag = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField( tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
@ -454,9 +534,9 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'device', 'name', 'type', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', 'tagged_vlans', 'tags', 'count_ipaddresses',
] ]
# TODO: This validation should be handled by Interface.clean() # TODO: This validation should be handled by Interface.clean()
@ -482,7 +562,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer): class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES) type = ChoiceField(choices=PortTypeChoices)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
@ -504,7 +584,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer): class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PORT_TYPE_CHOICES) type = ChoiceField(choices=PortTypeChoices)
rear_port = FrontPortRearPortSerializer() rear_port = FrontPortRearPortSerializer()
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
@ -549,15 +629,15 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
class CableSerializer(ValidatedModelSerializer): class CableSerializer(ValidatedModelSerializer):
termination_a_type = ContentTypeField( termination_a_type = ContentTypeField(
queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES) queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
) )
termination_b_type = ContentTypeField( termination_b_type = ContentTypeField(
queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES) queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
) )
termination_a = serializers.SerializerMethodField(read_only=True) termination_a = serializers.SerializerMethodField(read_only=True)
termination_b = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True)
status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) status = ChoiceField(choices=CableStatusChoices, required=False)
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
class Meta: class Meta:
model = Cable model = Cable
@ -662,20 +742,20 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
default=None default=None
) )
type = ChoiceField( type = ChoiceField(
choices=POWERFEED_TYPE_CHOICES, choices=PowerFeedTypeChoices,
default=POWERFEED_TYPE_PRIMARY default=PowerFeedTypeChoices.TYPE_PRIMARY
) )
status = ChoiceField( status = ChoiceField(
choices=POWERFEED_STATUS_CHOICES, choices=PowerFeedStatusChoices,
default=POWERFEED_STATUS_ACTIVE default=PowerFeedStatusChoices.STATUS_ACTIVE
) )
supply = ChoiceField( supply = ChoiceField(
choices=POWERFEED_SUPPLY_CHOICES, choices=PowerFeedSupplyChoices,
default=POWERFEED_SUPPLY_AC default=PowerFeedSupplyChoices.SUPPLY_AC
) )
phase = ChoiceField( phase = ChoiceField(
choices=POWERFEED_PHASE_CHOICES, choices=PowerFeedPhaseChoices,
default=POWERFEED_PHASE_SINGLE default=PowerFeedPhaseChoices.PHASE_SINGLE
) )
tags = TagListSerializerField( tags = TagListSerializerField(
required=False required=False

View File

@ -15,65 +15,65 @@ router = routers.DefaultRouter()
router.APIRootView = DCIMRootView router.APIRootView = DCIMRootView
# Field choices # Field choices
router.register(r'_choices', views.DCIMFieldChoicesViewSet, basename='field-choice') router.register('_choices', views.DCIMFieldChoicesViewSet, basename='field-choice')
# Sites # Sites
router.register(r'regions', views.RegionViewSet) router.register('regions', views.RegionViewSet)
router.register(r'sites', views.SiteViewSet) router.register('sites', views.SiteViewSet)
# Racks # Racks
router.register(r'rack-groups', views.RackGroupViewSet) router.register('rack-groups', views.RackGroupViewSet)
router.register(r'rack-roles', views.RackRoleViewSet) router.register('rack-roles', views.RackRoleViewSet)
router.register(r'racks', views.RackViewSet) router.register('racks', views.RackViewSet)
router.register(r'rack-reservations', views.RackReservationViewSet) router.register('rack-reservations', views.RackReservationViewSet)
# Device types # Device types
router.register(r'manufacturers', views.ManufacturerViewSet) router.register('manufacturers', views.ManufacturerViewSet)
router.register(r'device-types', views.DeviceTypeViewSet) router.register('device-types', views.DeviceTypeViewSet)
# Device type components # Device type components
router.register(r'console-port-templates', views.ConsolePortTemplateViewSet) router.register('console-port-templates', views.ConsolePortTemplateViewSet)
router.register(r'console-server-port-templates', views.ConsoleServerPortTemplateViewSet) router.register('console-server-port-templates', views.ConsoleServerPortTemplateViewSet)
router.register(r'power-port-templates', views.PowerPortTemplateViewSet) router.register('power-port-templates', views.PowerPortTemplateViewSet)
router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet) router.register('power-outlet-templates', views.PowerOutletTemplateViewSet)
router.register(r'interface-templates', views.InterfaceTemplateViewSet) router.register('interface-templates', views.InterfaceTemplateViewSet)
router.register(r'front-port-templates', views.FrontPortTemplateViewSet) router.register('front-port-templates', views.FrontPortTemplateViewSet)
router.register(r'rear-port-templates', views.RearPortTemplateViewSet) router.register('rear-port-templates', views.RearPortTemplateViewSet)
router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet) router.register('device-bay-templates', views.DeviceBayTemplateViewSet)
# Devices # Devices
router.register(r'device-roles', views.DeviceRoleViewSet) router.register('device-roles', views.DeviceRoleViewSet)
router.register(r'platforms', views.PlatformViewSet) router.register('platforms', views.PlatformViewSet)
router.register(r'devices', views.DeviceViewSet) router.register('devices', views.DeviceViewSet)
# Device components # Device components
router.register(r'console-ports', views.ConsolePortViewSet) router.register('console-ports', views.ConsolePortViewSet)
router.register(r'console-server-ports', views.ConsoleServerPortViewSet) router.register('console-server-ports', views.ConsoleServerPortViewSet)
router.register(r'power-ports', views.PowerPortViewSet) router.register('power-ports', views.PowerPortViewSet)
router.register(r'power-outlets', views.PowerOutletViewSet) router.register('power-outlets', views.PowerOutletViewSet)
router.register(r'interfaces', views.InterfaceViewSet) router.register('interfaces', views.InterfaceViewSet)
router.register(r'front-ports', views.FrontPortViewSet) router.register('front-ports', views.FrontPortViewSet)
router.register(r'rear-ports', views.RearPortViewSet) router.register('rear-ports', views.RearPortViewSet)
router.register(r'device-bays', views.DeviceBayViewSet) router.register('device-bays', views.DeviceBayViewSet)
router.register(r'inventory-items', views.InventoryItemViewSet) router.register('inventory-items', views.InventoryItemViewSet)
# Connections # Connections
router.register(r'console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections') router.register('console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections')
router.register(r'power-connections', views.PowerConnectionViewSet, basename='powerconnections') router.register('power-connections', views.PowerConnectionViewSet, basename='powerconnections')
router.register(r'interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections') router.register('interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections')
# Cables # Cables
router.register(r'cables', views.CableViewSet) router.register('cables', views.CableViewSet)
# Virtual chassis # Virtual chassis
router.register(r'virtual-chassis', views.VirtualChassisViewSet) router.register('virtual-chassis', views.VirtualChassisViewSet)
# Power # Power
router.register(r'power-panels', views.PowerPanelViewSet) router.register('power-panels', views.PowerPanelViewSet)
router.register(r'power-feeds', views.PowerFeedViewSet) router.register('power-feeds', views.PowerFeedViewSet)
# Miscellaneous # Miscellaneous
router.register(r'connected-device', views.ConnectedDeviceViewSet, basename='connected-device') router.register('connected-device', views.ConnectedDeviceViewSet, basename='connected-device')
app_name = 'dcim-api' app_name = 'dcim-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -2,7 +2,7 @@ from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.db.models import Count, F from django.db.models import Count, F
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.openapi import Parameter from drf_yasg.openapi import Parameter
@ -23,7 +23,6 @@ from dcim.models import (
) )
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.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
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 (
@ -41,21 +40,26 @@ from .exceptions import MissingFilterException
class DCIMFieldChoicesViewSet(FieldChoicesViewSet): class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']), (serializers.CableSerializer, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
(ConsolePort, ['connection_status']), (serializers.ConsolePortSerializer, ['type', 'connection_status']),
(Device, ['face', 'status']), (serializers.ConsolePortTemplateSerializer, ['type']),
(DeviceType, ['subdevice_role']), (serializers.ConsoleServerPortSerializer, ['type']),
(FrontPort, ['type']), (serializers.ConsoleServerPortTemplateSerializer, ['type']),
(FrontPortTemplate, ['type']), (serializers.DeviceSerializer, ['face', 'status']),
(Interface, ['type', 'mode']), (serializers.DeviceTypeSerializer, ['subdevice_role']),
(InterfaceTemplate, ['type']), (serializers.FrontPortSerializer, ['type']),
(PowerOutlet, ['feed_leg']), (serializers.FrontPortTemplateSerializer, ['type']),
(PowerOutletTemplate, ['feed_leg']), (serializers.InterfaceSerializer, ['type', 'mode']),
(PowerPort, ['connection_status']), (serializers.InterfaceTemplateSerializer, ['type']),
(Rack, ['outer_unit', 'status', 'type', 'width']), (serializers.PowerFeedSerializer, ['phase', 'status', 'supply', 'type']),
(RearPort, ['type']), (serializers.PowerOutletSerializer, ['type', 'feed_leg']),
(RearPortTemplate, ['type']), (serializers.PowerOutletTemplateSerializer, ['type', 'feed_leg']),
(Site, ['status']), (serializers.PowerPortSerializer, ['type', 'connection_status']),
(serializers.PowerPortTemplateSerializer, ['type']),
(serializers.RackSerializer, ['outer_unit', 'status', 'type', 'width']),
(serializers.RearPortSerializer, ['type']),
(serializers.RearPortTemplateSerializer, ['type']),
(serializers.SiteSerializer, ['status']),
) )
@ -102,7 +106,7 @@ class RegionViewSet(CustomFieldModelViewSet):
site_count=Count('sites') site_count=Count('sites')
) )
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
filterset_class = filters.RegionFilter filterset_class = filters.RegionFilterSet
# #
@ -121,7 +125,7 @@ class SiteViewSet(CustomFieldModelViewSet):
virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'), virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
) )
serializer_class = serializers.SiteSerializer serializer_class = serializers.SiteSerializer
filterset_class = filters.SiteFilter filterset_class = filters.SiteFilterSet
@action(detail=True) @action(detail=True)
def graphs(self, request, pk): def graphs(self, request, pk):
@ -129,7 +133,7 @@ class SiteViewSet(CustomFieldModelViewSet):
A convenience method for rendering graphs for a particular site. A convenience method for rendering graphs for a particular site.
""" """
site = get_object_or_404(Site, pk=pk) site = get_object_or_404(Site, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE) queryset = Graph.objects.filter(type__model='site')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
return Response(serializer.data) return Response(serializer.data)
@ -143,7 +147,7 @@ class RackGroupViewSet(ModelViewSet):
rack_count=Count('racks') rack_count=Count('racks')
) )
serializer_class = serializers.RackGroupSerializer serializer_class = serializers.RackGroupSerializer
filterset_class = filters.RackGroupFilter filterset_class = filters.RackGroupFilterSet
# #
@ -155,7 +159,7 @@ class RackRoleViewSet(ModelViewSet):
rack_count=Count('racks') rack_count=Count('racks')
) )
serializer_class = serializers.RackRoleSerializer serializer_class = serializers.RackRoleSerializer
filterset_class = filters.RackRoleFilter filterset_class = filters.RackRoleFilterSet
# #
@ -170,15 +174,17 @@ class RackViewSet(CustomFieldModelViewSet):
powerfeed_count=get_subquery(PowerFeed, 'rack') powerfeed_count=get_subquery(PowerFeed, 'rack')
) )
serializer_class = serializers.RackSerializer serializer_class = serializers.RackSerializer
filterset_class = filters.RackFilter filterset_class = filters.RackFilterSet
@swagger_auto_schema(deprecated=True)
@action(detail=True) @action(detail=True)
def units(self, request, pk=None): def units(self, request, pk=None):
""" """
List rack units (by rack) List rack units (by rack)
""" """
# TODO: Remove this action detail route in v2.8
rack = get_object_or_404(Rack, pk=pk) rack = get_object_or_404(Rack, pk=pk)
face = request.GET.get('face', 0) face = request.GET.get('face', 'front')
exclude_pk = request.GET.get('exclude', None) exclude_pk = request.GET.get('exclude', None)
if exclude_pk is not None: if exclude_pk is not None:
try: try:
@ -197,6 +203,50 @@ class RackViewSet(CustomFieldModelViewSet):
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request}) rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(rack_units.data) return self.get_paginated_response(rack_units.data)
@swagger_auto_schema(
responses={200: serializers.RackUnitSerializer(many=True)},
query_serializer=serializers.RackElevationDetailFilterSerializer
)
@action(detail=True)
def elevation(self, request, pk=None):
"""
Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
"""
rack = get_object_or_404(Rack, pk=pk)
serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
if not serializer.is_valid():
return Response(serializer.errors, 400)
data = serializer.validated_data
if data['render'] == 'svg':
# Render and return the elevation as an SVG drawing with the correct content type
drawing = rack.get_elevation_svg(
face=data['face'],
unit_width=data['unit_width'],
unit_height=data['unit_height'],
legend_width=data['legend_width'],
include_images=data['include_images']
)
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
else:
# Return a JSON representation of the rack units in the elevation
elevation = rack.get_rack_units(
face=data['face'],
exclude=data['exclude'],
expand_devices=data['expand_devices']
)
# Enable filtering rack units by ID
q = data['q']
if q:
elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name'])]
page = self.paginate_queryset(elevation)
if page is not None:
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(rack_units.data)
# #
# Rack reservations # Rack reservations
@ -205,7 +255,7 @@ class RackViewSet(CustomFieldModelViewSet):
class RackReservationViewSet(ModelViewSet): class RackReservationViewSet(ModelViewSet):
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant') queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
serializer_class = serializers.RackReservationSerializer serializer_class = serializers.RackReservationSerializer
filterset_class = filters.RackReservationFilter filterset_class = filters.RackReservationFilterSet
# Assign user from request # Assign user from request
def perform_create(self, serializer): def perform_create(self, serializer):
@ -223,7 +273,7 @@ class ManufacturerViewSet(ModelViewSet):
platform_count=get_subquery(Platform, 'manufacturer') platform_count=get_subquery(Platform, 'manufacturer')
) )
serializer_class = serializers.ManufacturerSerializer serializer_class = serializers.ManufacturerSerializer
filterset_class = filters.ManufacturerFilter filterset_class = filters.ManufacturerFilterSet
# #
@ -235,7 +285,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
device_count=Count('instances') device_count=Count('instances')
) )
serializer_class = serializers.DeviceTypeSerializer serializer_class = serializers.DeviceTypeSerializer
filterset_class = filters.DeviceTypeFilter filterset_class = filters.DeviceTypeFilterSet
# #
@ -245,49 +295,49 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
class ConsolePortTemplateViewSet(ModelViewSet): class ConsolePortTemplateViewSet(ModelViewSet):
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsolePortTemplateSerializer serializer_class = serializers.ConsolePortTemplateSerializer
filterset_class = filters.ConsolePortTemplateFilter filterset_class = filters.ConsolePortTemplateFilterSet
class ConsoleServerPortTemplateViewSet(ModelViewSet): class ConsoleServerPortTemplateViewSet(ModelViewSet):
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsoleServerPortTemplateSerializer serializer_class = serializers.ConsoleServerPortTemplateSerializer
filterset_class = filters.ConsoleServerPortTemplateFilter filterset_class = filters.ConsoleServerPortTemplateFilterSet
class PowerPortTemplateViewSet(ModelViewSet): class PowerPortTemplateViewSet(ModelViewSet):
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerPortTemplateSerializer serializer_class = serializers.PowerPortTemplateSerializer
filterset_class = filters.PowerPortTemplateFilter filterset_class = filters.PowerPortTemplateFilterSet
class PowerOutletTemplateViewSet(ModelViewSet): class PowerOutletTemplateViewSet(ModelViewSet):
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerOutletTemplateSerializer serializer_class = serializers.PowerOutletTemplateSerializer
filterset_class = filters.PowerOutletTemplateFilter filterset_class = filters.PowerOutletTemplateFilterSet
class InterfaceTemplateViewSet(ModelViewSet): class InterfaceTemplateViewSet(ModelViewSet):
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer') queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.InterfaceTemplateSerializer serializer_class = serializers.InterfaceTemplateSerializer
filterset_class = filters.InterfaceTemplateFilter filterset_class = filters.InterfaceTemplateFilterSet
class FrontPortTemplateViewSet(ModelViewSet): class FrontPortTemplateViewSet(ModelViewSet):
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.FrontPortTemplateSerializer serializer_class = serializers.FrontPortTemplateSerializer
filterset_class = filters.FrontPortTemplateFilter filterset_class = filters.FrontPortTemplateFilterSet
class RearPortTemplateViewSet(ModelViewSet): class RearPortTemplateViewSet(ModelViewSet):
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.RearPortTemplateSerializer serializer_class = serializers.RearPortTemplateSerializer
filterset_class = filters.RearPortTemplateFilter filterset_class = filters.RearPortTemplateFilterSet
class DeviceBayTemplateViewSet(ModelViewSet): class DeviceBayTemplateViewSet(ModelViewSet):
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer') queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer serializer_class = serializers.DeviceBayTemplateSerializer
filterset_class = filters.DeviceBayTemplateFilter filterset_class = filters.DeviceBayTemplateFilterSet
# #
@ -300,7 +350,7 @@ class DeviceRoleViewSet(ModelViewSet):
virtualmachine_count=get_subquery(VirtualMachine, 'role') virtualmachine_count=get_subquery(VirtualMachine, 'role')
) )
serializer_class = serializers.DeviceRoleSerializer serializer_class = serializers.DeviceRoleSerializer
filterset_class = filters.DeviceRoleFilter filterset_class = filters.DeviceRoleFilterSet
# #
@ -313,7 +363,7 @@ class PlatformViewSet(ModelViewSet):
virtualmachine_count=get_subquery(VirtualMachine, 'platform') virtualmachine_count=get_subquery(VirtualMachine, 'platform')
) )
serializer_class = serializers.PlatformSerializer serializer_class = serializers.PlatformSerializer
filterset_class = filters.PlatformFilter filterset_class = filters.PlatformFilterSet
# #
@ -325,7 +375,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay', 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags', 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
) )
filterset_class = filters.DeviceFilter filterset_class = filters.DeviceFilterSet
def get_serializer_class(self): def get_serializer_class(self):
""" """
@ -353,11 +403,22 @@ class DeviceViewSet(CustomFieldModelViewSet):
A convenience method for rendering graphs for a particular Device. A convenience method for rendering graphs for a particular Device.
""" """
device = get_object_or_404(Device, pk=pk) device = get_object_or_404(Device, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_DEVICE) queryset = Graph.objects.filter(type__model='device')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
return Response(serializer.data) return Response(serializer.data)
@swagger_auto_schema(
manual_parameters=[
Parameter(
name='method',
in_='query',
required=True,
type=openapi.TYPE_STRING
)
],
responses={'200': serializers.DeviceNAPALMSerializer}
)
@action(detail=True, url_path='napalm') @action(detail=True, url_path='napalm')
def napalm(self, request, pk): def napalm(self, request, pk):
""" """
@ -396,13 +457,29 @@ class DeviceViewSet(CustomFieldModelViewSet):
napalm_methods = request.GET.getlist('method') napalm_methods = request.GET.getlist('method')
response = OrderedDict([(m, None) for m in napalm_methods]) response = OrderedDict([(m, None) for m in napalm_methods])
ip_address = str(device.primary_ip.address.ip) ip_address = str(device.primary_ip.address.ip)
username = settings.NAPALM_USERNAME
password = settings.NAPALM_PASSWORD
optional_args = settings.NAPALM_ARGS.copy() optional_args = settings.NAPALM_ARGS.copy()
if device.platform.napalm_args is not None: if device.platform.napalm_args is not None:
optional_args.update(device.platform.napalm_args) optional_args.update(device.platform.napalm_args)
# Update NAPALM parameters according to the request headers
for header in request.headers:
if header[:9].lower() != 'x-napalm-':
continue
key = header[9:]
if key.lower() == 'username':
username = request.headers[header]
elif key.lower() == 'password':
password = request.headers[header]
elif key:
optional_args[key.lower()] = request.headers[header]
d = driver( d = driver(
hostname=ip_address, hostname=ip_address,
username=settings.NAPALM_USERNAME, username=username,
password=settings.NAPALM_PASSWORD, password=password,
timeout=settings.NAPALM_TIMEOUT, timeout=settings.NAPALM_TIMEOUT,
optional_args=optional_args optional_args=optional_args
) )
@ -437,13 +514,13 @@ class DeviceViewSet(CustomFieldModelViewSet):
class ConsolePortViewSet(CableTraceMixin, ModelViewSet): class ConsolePortViewSet(CableTraceMixin, ModelViewSet):
queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsolePortFilter filterset_class = filters.ConsolePortFilterSet
class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet): class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
serializer_class = serializers.ConsoleServerPortSerializer serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filters.ConsoleServerPortFilter filterset_class = filters.ConsoleServerPortFilterSet
class PowerPortViewSet(CableTraceMixin, ModelViewSet): class PowerPortViewSet(CableTraceMixin, ModelViewSet):
@ -451,13 +528,13 @@ class PowerPortViewSet(CableTraceMixin, ModelViewSet):
'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags' 'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags'
) )
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerPortFilter filterset_class = filters.PowerPortFilterSet
class PowerOutletViewSet(CableTraceMixin, ModelViewSet): class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
serializer_class = serializers.PowerOutletSerializer serializer_class = serializers.PowerOutletSerializer
filterset_class = filters.PowerOutletFilter filterset_class = filters.PowerOutletFilterSet
class InterfaceViewSet(CableTraceMixin, ModelViewSet): class InterfaceViewSet(CableTraceMixin, ModelViewSet):
@ -467,7 +544,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
device__isnull=False device__isnull=False
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
filterset_class = filters.InterfaceFilter filterset_class = filters.InterfaceFilterSet
@action(detail=True) @action(detail=True)
def graphs(self, request, pk): def graphs(self, request, pk):
@ -475,7 +552,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
A convenience method for rendering graphs for a particular interface. A convenience method for rendering graphs for a particular interface.
""" """
interface = get_object_or_404(Interface, pk=pk) interface = get_object_or_404(Interface, pk=pk)
queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE) queryset = Graph.objects.filter(type__model='interface')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
return Response(serializer.data) return Response(serializer.data)
@ -483,25 +560,25 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
class FrontPortViewSet(ModelViewSet): class FrontPortViewSet(ModelViewSet):
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
serializer_class = serializers.FrontPortSerializer serializer_class = serializers.FrontPortSerializer
filterset_class = filters.FrontPortFilter filterset_class = filters.FrontPortFilterSet
class RearPortViewSet(ModelViewSet): class RearPortViewSet(ModelViewSet):
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
serializer_class = serializers.RearPortSerializer serializer_class = serializers.RearPortSerializer
filterset_class = filters.RearPortFilter filterset_class = filters.RearPortFilterSet
class DeviceBayViewSet(ModelViewSet): class DeviceBayViewSet(ModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags') queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
serializer_class = serializers.DeviceBaySerializer serializer_class = serializers.DeviceBaySerializer
filterset_class = filters.DeviceBayFilter filterset_class = filters.DeviceBayFilterSet
class InventoryItemViewSet(ModelViewSet): class InventoryItemViewSet(ModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
serializer_class = serializers.InventoryItemSerializer serializer_class = serializers.InventoryItemSerializer
filterset_class = filters.InventoryItemFilter filterset_class = filters.InventoryItemFilterSet
# #
@ -515,7 +592,7 @@ class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
connected_endpoint__isnull=False connected_endpoint__isnull=False
) )
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsoleConnectionFilter filterset_class = filters.ConsoleConnectionFilterSet
class PowerConnectionViewSet(ListModelMixin, GenericViewSet): class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
@ -525,7 +602,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
_connected_poweroutlet__isnull=False _connected_poweroutlet__isnull=False
) )
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerConnectionFilter filterset_class = filters.PowerConnectionFilterSet
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
@ -537,7 +614,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
pk__lt=F('_connected_interface') pk__lt=F('_connected_interface')
) )
serializer_class = serializers.InterfaceConnectionSerializer serializer_class = serializers.InterfaceConnectionSerializer
filterset_class = filters.InterfaceConnectionFilter filterset_class = filters.InterfaceConnectionFilterSet
# #
@ -549,7 +626,7 @@ class CableViewSet(ModelViewSet):
'termination_a', 'termination_b' 'termination_a', 'termination_b'
) )
serializer_class = serializers.CableSerializer serializer_class = serializers.CableSerializer
filterset_class = filters.CableFilter filterset_class = filters.CableFilterSet
# #
@ -561,7 +638,7 @@ class VirtualChassisViewSet(ModelViewSet):
member_count=Count('members') member_count=Count('members')
) )
serializer_class = serializers.VirtualChassisSerializer serializer_class = serializers.VirtualChassisSerializer
filterset_class = filters.VirtualChassisFilter filterset_class = filters.VirtualChassisFilterSet
# #
@ -575,7 +652,7 @@ class PowerPanelViewSet(ModelViewSet):
powerfeed_count=Count('powerfeeds') powerfeed_count=Count('powerfeeds')
) )
serializer_class = serializers.PowerPanelSerializer serializer_class = serializers.PowerPanelSerializer
filterset_class = filters.PowerPanelFilter filterset_class = filters.PowerPanelFilterSet
# #
@ -585,7 +662,7 @@ class PowerPanelViewSet(ModelViewSet):
class PowerFeedViewSet(CustomFieldModelViewSet): class PowerFeedViewSet(CustomFieldModelViewSet):
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags') queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags')
serializer_class = serializers.PowerFeedSerializer serializer_class = serializers.PowerFeedSerializer
filterset_class = filters.PowerFeedFilter filterset_class = filters.PowerFeedFilterSet
# #

1092
netbox/dcim/choices.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,461 +1,89 @@
from django.db.models import Q
# Rack types from .choices import InterfaceTypeChoices
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
# Rack widths
RACK_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
# Rack faces
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
[RACK_FACE_FRONT, 'Front'],
[RACK_FACE_REAR, 'Rear'],
]
# Rack statuses
RACK_STATUS_RESERVED = 0
RACK_STATUS_AVAILABLE = 1
RACK_STATUS_PLANNED = 2
RACK_STATUS_ACTIVE = 3
RACK_STATUS_DEPRECATED = 4
RACK_STATUS_CHOICES = [
[RACK_STATUS_ACTIVE, 'Active'],
[RACK_STATUS_PLANNED, 'Planned'],
[RACK_STATUS_RESERVED, 'Reserved'],
[RACK_STATUS_AVAILABLE, 'Available'],
[RACK_STATUS_DEPRECATED, 'Deprecated'],
]
# Device rack position
DEVICE_POSITION_CHOICES = [
# Rack.u_height is limited to 100
(i, 'Unit {}'.format(i)) for i in range(1, 101)
]
# Parent/child device roles
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
# Interface types
# Virtual
IFACE_TYPE_VIRTUAL = 0
IFACE_TYPE_LAG = 200
# Ethernet
IFACE_TYPE_100ME_FIXED = 800
IFACE_TYPE_1GE_FIXED = 1000
IFACE_TYPE_1GE_GBIC = 1050
IFACE_TYPE_1GE_SFP = 1100
IFACE_TYPE_2GE_FIXED = 1120
IFACE_TYPE_5GE_FIXED = 1130
IFACE_TYPE_10GE_FIXED = 1150
IFACE_TYPE_10GE_CX4 = 1170
IFACE_TYPE_10GE_SFP_PLUS = 1200
IFACE_TYPE_10GE_XFP = 1300
IFACE_TYPE_10GE_XENPAK = 1310
IFACE_TYPE_10GE_X2 = 1320
IFACE_TYPE_25GE_SFP28 = 1350
IFACE_TYPE_40GE_QSFP_PLUS = 1400
IFACE_TYPE_50GE_QSFP28 = 1420
IFACE_TYPE_100GE_CFP = 1500
IFACE_TYPE_100GE_CFP2 = 1510
IFACE_TYPE_100GE_CFP4 = 1520
IFACE_TYPE_100GE_CPAK = 1550
IFACE_TYPE_100GE_QSFP28 = 1600
IFACE_TYPE_200GE_CFP2 = 1650
IFACE_TYPE_200GE_QSFP56 = 1700
IFACE_TYPE_400GE_QSFP_DD = 1750
IFACE_TYPE_400GE_OSFP = 1800
# Wireless
IFACE_TYPE_80211A = 2600
IFACE_TYPE_80211G = 2610
IFACE_TYPE_80211N = 2620
IFACE_TYPE_80211AC = 2630
IFACE_TYPE_80211AD = 2640
# Cellular
IFACE_TYPE_GSM = 2810
IFACE_TYPE_CDMA = 2820
IFACE_TYPE_LTE = 2830
# SONET
IFACE_TYPE_SONET_OC3 = 6100
IFACE_TYPE_SONET_OC12 = 6200
IFACE_TYPE_SONET_OC48 = 6300
IFACE_TYPE_SONET_OC192 = 6400
IFACE_TYPE_SONET_OC768 = 6500
IFACE_TYPE_SONET_OC1920 = 6600
IFACE_TYPE_SONET_OC3840 = 6700
# Fibrechannel
IFACE_TYPE_1GFC_SFP = 3010
IFACE_TYPE_2GFC_SFP = 3020
IFACE_TYPE_4GFC_SFP = 3040
IFACE_TYPE_8GFC_SFP_PLUS = 3080
IFACE_TYPE_16GFC_SFP_PLUS = 3160
IFACE_TYPE_32GFC_SFP28 = 3320
IFACE_TYPE_128GFC_QSFP28 = 3400
# InfiniBand
IFACE_FF_INFINIBAND_SDR = 7010
IFACE_FF_INFINIBAND_DDR = 7020
IFACE_FF_INFINIBAND_QDR = 7030
IFACE_FF_INFINIBAND_FDR10 = 7040
IFACE_FF_INFINIBAND_FDR = 7050
IFACE_FF_INFINIBAND_EDR = 7060
IFACE_FF_INFINIBAND_HDR = 7070
IFACE_FF_INFINIBAND_NDR = 7080
IFACE_FF_INFINIBAND_XDR = 7090
# Serial
IFACE_TYPE_T1 = 4000
IFACE_TYPE_E1 = 4010
IFACE_TYPE_T3 = 4040
IFACE_TYPE_E3 = 4050
# Stacking
IFACE_TYPE_STACKWISE = 5000
IFACE_TYPE_STACKWISE_PLUS = 5050
IFACE_TYPE_FLEXSTACK = 5100
IFACE_TYPE_FLEXSTACK_PLUS = 5150
IFACE_TYPE_JUNIPER_VCP = 5200
IFACE_TYPE_SUMMITSTACK = 5300
IFACE_TYPE_SUMMITSTACK128 = 5310
IFACE_TYPE_SUMMITSTACK256 = 5320
IFACE_TYPE_SUMMITSTACK512 = 5330
# Other
IFACE_TYPE_OTHER = 32767
IFACE_TYPE_KEYSTONE = 32766
IFACE_TYPE_CHOICES = [ #
[ # Racks
'Virtual interfaces', #
[
[IFACE_TYPE_VIRTUAL, 'Virtual'], RACK_U_HEIGHT_DEFAULT = 42
[IFACE_TYPE_LAG, 'Link Aggregation Group (LAG)'],
], RACK_ELEVATION_BORDER_WIDTH = 2
], RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
[ RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 220
'Ethernet (fixed)', RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 22
[
[IFACE_TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_TYPE_1GE_FIXED, '1000BASE-T (1GE)'], #
[IFACE_TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'], # RearPorts
[IFACE_TYPE_5GE_FIXED, '5GBASE-T (5GE)'], #
[IFACE_TYPE_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'], REARPORT_POSITIONS_MIN = 1
] REARPORT_POSITIONS_MAX = 64
],
[
'Ethernet (modular)', #
[ # Interfaces
[IFACE_TYPE_1GE_GBIC, 'GBIC (1GE)'], #
[IFACE_TYPE_1GE_SFP, 'SFP (1GE)'],
[IFACE_TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'], INTERFACE_MTU_MIN = 1
[IFACE_TYPE_10GE_XFP, 'XFP (10GE)'], INTERFACE_MTU_MAX = 32767 # Max value of a signed 16-bit integer
[IFACE_TYPE_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_TYPE_10GE_X2, 'X2 (10GE)'],
[IFACE_TYPE_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_TYPE_50GE_QSFP28, 'QSFP28 (50GE)'],
[IFACE_TYPE_100GE_CFP, 'CFP (100GE)'],
[IFACE_TYPE_100GE_CFP2, 'CFP2 (100GE)'],
[IFACE_TYPE_200GE_CFP2, 'CFP2 (200GE)'],
[IFACE_TYPE_100GE_CFP4, 'CFP4 (100GE)'],
[IFACE_TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'],
[IFACE_TYPE_100GE_QSFP28, 'QSFP28 (100GE)'],
[IFACE_TYPE_200GE_QSFP56, 'QSFP56 (200GE)'],
[IFACE_TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
[IFACE_TYPE_400GE_OSFP, 'OSFP (400GE)'],
]
],
[
'Wireless',
[
[IFACE_TYPE_80211A, 'IEEE 802.11a'],
[IFACE_TYPE_80211G, 'IEEE 802.11b/g'],
[IFACE_TYPE_80211N, 'IEEE 802.11n'],
[IFACE_TYPE_80211AC, 'IEEE 802.11ac'],
[IFACE_TYPE_80211AD, 'IEEE 802.11ad'],
]
],
[
'Cellular',
[
[IFACE_TYPE_GSM, 'GSM'],
[IFACE_TYPE_CDMA, 'CDMA'],
[IFACE_TYPE_LTE, 'LTE'],
]
],
[
'SONET',
[
[IFACE_TYPE_SONET_OC3, 'OC-3/STM-1'],
[IFACE_TYPE_SONET_OC12, 'OC-12/STM-4'],
[IFACE_TYPE_SONET_OC48, 'OC-48/STM-16'],
[IFACE_TYPE_SONET_OC192, 'OC-192/STM-64'],
[IFACE_TYPE_SONET_OC768, 'OC-768/STM-256'],
[IFACE_TYPE_SONET_OC1920, 'OC-1920/STM-640'],
[IFACE_TYPE_SONET_OC3840, 'OC-3840/STM-1234'],
]
],
[
'FibreChannel',
[
[IFACE_TYPE_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_TYPE_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_TYPE_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
[IFACE_TYPE_32GFC_SFP28, 'SFP28 (32GFC)'],
[IFACE_TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'],
]
],
[
'InfiniBand',
[
[IFACE_FF_INFINIBAND_SDR, 'SDR (2 Gbps)'],
[IFACE_FF_INFINIBAND_DDR, 'DDR (4 Gbps)'],
[IFACE_FF_INFINIBAND_QDR, 'QDR (8 Gbps)'],
[IFACE_FF_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
[IFACE_FF_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
[IFACE_FF_INFINIBAND_EDR, 'EDR (25 Gbps)'],
[IFACE_FF_INFINIBAND_HDR, 'HDR (50 Gbps)'],
[IFACE_FF_INFINIBAND_NDR, 'NDR (100 Gbps)'],
[IFACE_FF_INFINIBAND_XDR, 'XDR (250 Gbps)'],
]
],
[
'Serial',
[
[IFACE_TYPE_T1, 'T1 (1.544 Mbps)'],
[IFACE_TYPE_E1, 'E1 (2.048 Mbps)'],
[IFACE_TYPE_T3, 'T3 (45 Mbps)'],
[IFACE_TYPE_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_TYPE_STACKWISE, 'Cisco StackWise'],
[IFACE_TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_TYPE_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_TYPE_JUNIPER_VCP, 'Juniper VCP'],
[IFACE_TYPE_SUMMITSTACK, 'Extreme SummitStack'],
[IFACE_TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'],
[IFACE_TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'],
[IFACE_TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'],
]
],
[
'Other',
[
[IFACE_TYPE_KEYSTONE, 'Keystone'],
[IFACE_TYPE_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [ VIRTUAL_IFACE_TYPES = [
IFACE_TYPE_VIRTUAL, InterfaceTypeChoices.TYPE_VIRTUAL,
IFACE_TYPE_LAG, InterfaceTypeChoices.TYPE_LAG,
] ]
WIRELESS_IFACE_TYPES = [ WIRELESS_IFACE_TYPES = [
IFACE_TYPE_80211A, InterfaceTypeChoices.TYPE_80211A,
IFACE_TYPE_80211G, InterfaceTypeChoices.TYPE_80211G,
IFACE_TYPE_80211N, InterfaceTypeChoices.TYPE_80211N,
IFACE_TYPE_80211AC, InterfaceTypeChoices.TYPE_80211AC,
IFACE_TYPE_80211AD, InterfaceTypeChoices.TYPE_80211AD,
] ]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
IFACE_MODE_ACCESS = 100
IFACE_MODE_TAGGED = 200
IFACE_MODE_TAGGED_ALL = 300
IFACE_MODE_CHOICES = [
[IFACE_MODE_ACCESS, 'Access'],
[IFACE_MODE_TAGGED, 'Tagged'],
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
]
# Pass-through port types #
PORT_TYPE_8P8C = 1000 # PowerFeeds
PORT_TYPE_110_PUNCH = 1100 #
PORT_TYPE_BNC = 1200
PORT_TYPE_ST = 2000
PORT_TYPE_SC = 2100
PORT_TYPE_SC_APC = 2110
PORT_TYPE_FC = 2200
PORT_TYPE_LC = 2300
PORT_TYPE_LC_APC = 2310
PORT_TYPE_MTRJ = 2400
PORT_TYPE_MPO = 2500
PORT_TYPE_LSH = 2600
PORT_TYPE_LSH_APC = 2610
PORT_TYPE_CHOICES = [
[
'Copper',
[
[PORT_TYPE_8P8C, '8P8C'],
[PORT_TYPE_110_PUNCH, '110 Punch'],
[PORT_TYPE_BNC, 'BNC'],
],
],
[
'Fiber Optic',
[
[PORT_TYPE_FC, 'FC'],
[PORT_TYPE_LC, 'LC'],
[PORT_TYPE_LC_APC, 'LC/APC'],
[PORT_TYPE_LSH, 'LSH'],
[PORT_TYPE_LSH_APC, 'LSH/APC'],
[PORT_TYPE_MPO, 'MPO'],
[PORT_TYPE_MTRJ, 'MTRJ'],
[PORT_TYPE_SC, 'SC'],
[PORT_TYPE_SC_APC, 'SC/APC'],
[PORT_TYPE_ST, 'ST'],
]
]
]
# Device statuses POWERFEED_VOLTAGE_DEFAULT = 120
DEVICE_STATUS_OFFLINE = 0
DEVICE_STATUS_ACTIVE = 1
DEVICE_STATUS_PLANNED = 2
DEVICE_STATUS_STAGED = 3
DEVICE_STATUS_FAILED = 4
DEVICE_STATUS_INVENTORY = 5
DEVICE_STATUS_DECOMMISSIONING = 6
DEVICE_STATUS_CHOICES = [
[DEVICE_STATUS_ACTIVE, 'Active'],
[DEVICE_STATUS_OFFLINE, 'Offline'],
[DEVICE_STATUS_PLANNED, 'Planned'],
[DEVICE_STATUS_STAGED, 'Staged'],
[DEVICE_STATUS_FAILED, 'Failed'],
[DEVICE_STATUS_INVENTORY, 'Inventory'],
[DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'],
]
# Site statuses POWERFEED_AMPERAGE_DEFAULT = 20
SITE_STATUS_ACTIVE = 1
SITE_STATUS_PLANNED = 2
SITE_STATUS_RETIRED = 4
SITE_STATUS_CONSTRUCTION = 90
SITE_STATUS_COMMISSIONING = 91
SITE_STATUS_CHOICES = [
[SITE_STATUS_ACTIVE, 'Active'],
[SITE_STATUS_PLANNED, 'Planned'],
[SITE_STATUS_CONSTRUCTION, 'Construction'],
[SITE_STATUS_COMMISSIONING, 'Commissioning'],
[SITE_STATUS_RETIRED, 'Retired'],
]
# Bootstrap CSS classes for device/rack statuses POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
STATUS_CLASSES = {
0: 'warning',
1: 'success', #
2: 'info', # Cabling and connections
3: 'primary', #
4: 'danger',
5: 'default',
6: 'warning',
}
# Console/power/interface connection statuses # Console/power/interface connection statuses
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
CONNECTION_STATUS_CHOICES = [ CONNECTION_STATUS_CHOICES = [
[CONNECTION_STATUS_PLANNED, 'Planned'], [False, 'Not Connected'],
[CONNECTION_STATUS_CONNECTED, 'Connected'], [True, 'Connected'],
] ]
# Cable endpoint types # Cable endpoint types
CABLE_TERMINATION_TYPES = [ CABLE_TERMINATION_MODELS = Q(
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', Q(app_label='circuits', model__in=(
'circuittermination', 'powerfeed', 'circuittermination',
] )) |
Q(app_label='dcim', model__in=(
# Cable types 'consoleport',
CABLE_TYPE_CAT3 = 1300 'consoleserverport',
CABLE_TYPE_CAT5 = 1500 'frontport',
CABLE_TYPE_CAT5E = 1510 'interface',
CABLE_TYPE_CAT6 = 1600 'powerfeed',
CABLE_TYPE_CAT6A = 1610 'poweroutlet',
CABLE_TYPE_CAT7 = 1700 'powerport',
CABLE_TYPE_DAC_ACTIVE = 1800 'rearport',
CABLE_TYPE_DAC_PASSIVE = 1810 ))
CABLE_TYPE_COAXIAL = 1900
CABLE_TYPE_MMF = 3000
CABLE_TYPE_MMF_OM1 = 3010
CABLE_TYPE_MMF_OM2 = 3020
CABLE_TYPE_MMF_OM3 = 3030
CABLE_TYPE_MMF_OM4 = 3040
CABLE_TYPE_SMF = 3500
CABLE_TYPE_SMF_OS1 = 3510
CABLE_TYPE_SMF_OS2 = 3520
CABLE_TYPE_AOC = 3800
CABLE_TYPE_POWER = 5000
CABLE_TYPE_CHOICES = (
(
'Copper', (
(CABLE_TYPE_CAT3, 'CAT3'),
(CABLE_TYPE_CAT5, 'CAT5'),
(CABLE_TYPE_CAT5E, 'CAT5e'),
(CABLE_TYPE_CAT6, 'CAT6'),
(CABLE_TYPE_CAT6A, 'CAT6a'),
(CABLE_TYPE_CAT7, 'CAT7'),
(CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
(CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
(CABLE_TYPE_COAXIAL, 'Coaxial'),
),
),
(
'Fiber', (
(CABLE_TYPE_MMF, 'Multimode Fiber'),
(CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
(CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
(CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(CABLE_TYPE_SMF, 'Singlemode Fiber'),
(CABLE_TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
(CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
(CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
),
),
(CABLE_TYPE_POWER, 'Power'),
) )
CABLE_TERMINATION_TYPE_CHOICES = {
# (API endpoint, human-friendly name)
'consoleport': ('console-ports', 'Console port'),
'consoleserverport': ('console-server-ports', 'Console server port'),
'powerport': ('power-ports', 'Power port'),
'poweroutlet': ('power-outlets', 'Power outlet'),
'interface': ('interfaces', 'Interface'),
'frontport': ('front-ports', 'Front panel port'),
'rearport': ('rear-ports', 'Rear panel port'),
}
COMPATIBLE_TERMINATION_TYPES = { COMPATIBLE_TERMINATION_TYPES = {
'consoleport': ['consoleserverport', 'frontport', 'rearport'], 'consoleport': ['consoleserverport', 'frontport', 'rearport'],
'consoleserverport': ['consoleport', 'frontport', 'rearport'], 'consoleserverport': ['consoleport', 'frontport', 'rearport'],
@ -466,57 +94,3 @@ COMPATIBLE_TERMINATION_TYPES = {
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'], 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
'circuittermination': ['interface', 'frontport', 'rearport'], 'circuittermination': ['interface', 'frontport', 'rearport'],
} }
LENGTH_UNIT_METER = 1200
LENGTH_UNIT_CENTIMETER = 1100
LENGTH_UNIT_MILLIMETER = 1000
LENGTH_UNIT_FOOT = 2100
LENGTH_UNIT_INCH = 2000
CABLE_LENGTH_UNIT_CHOICES = (
(LENGTH_UNIT_METER, 'Meters'),
(LENGTH_UNIT_CENTIMETER, 'Centimeters'),
(LENGTH_UNIT_FOOT, 'Feet'),
(LENGTH_UNIT_INCH, 'Inches'),
)
RACK_DIMENSION_UNIT_CHOICES = (
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
(LENGTH_UNIT_INCH, 'Inches'),
)
# Power feeds
POWERFEED_TYPE_PRIMARY = 1
POWERFEED_TYPE_REDUNDANT = 2
POWERFEED_TYPE_CHOICES = (
(POWERFEED_TYPE_PRIMARY, 'Primary'),
(POWERFEED_TYPE_REDUNDANT, 'Redundant'),
)
POWERFEED_SUPPLY_AC = 1
POWERFEED_SUPPLY_DC = 2
POWERFEED_SUPPLY_CHOICES = (
(POWERFEED_SUPPLY_AC, 'AC'),
(POWERFEED_SUPPLY_DC, 'DC'),
)
POWERFEED_PHASE_SINGLE = 1
POWERFEED_PHASE_3PHASE = 3
POWERFEED_PHASE_CHOICES = (
(POWERFEED_PHASE_SINGLE, 'Single phase'),
(POWERFEED_PHASE_3PHASE, 'Three-phase'),
)
POWERFEED_STATUS_OFFLINE = 0
POWERFEED_STATUS_ACTIVE = 1
POWERFEED_STATUS_PLANNED = 2
POWERFEED_STATUS_FAILED = 4
POWERFEED_STATUS_CHOICES = (
(POWERFEED_STATUS_ACTIVE, 'Active'),
(POWERFEED_STATUS_OFFLINE, 'Offline'),
(POWERFEED_STATUS_PLANNED, 'Planned'),
(POWERFEED_STATUS_FAILED, 'Failed'),
)
POWERFEED_LEG_A = 1
POWERFEED_LEG_B = 2
POWERFEED_LEG_C = 3
POWERFEED_LEG_CHOICES = (
(POWERFEED_LEG_A, 'A'),
(POWERFEED_LEG_B, 'B'),
(POWERFEED_LEG_C, 'C'),
)

204
netbox/dcim/elevations.py Normal file
View File

@ -0,0 +1,204 @@
import svgwrite
from django.conf import settings
from django.urls import reverse
from django.utils.http import urlencode
from utilities.utils import foreground_color
from .choices import DeviceFaceChoices
from .constants import RACK_ELEVATION_BORDER_WIDTH
class RackElevationSVG:
"""
Use this class to render a rack elevation as an SVG image.
:param rack: A NetBox Rack instance
:param include_images: If true, the SVG document will embed front/rear device face images, where available
"""
def __init__(self, rack, include_images=True):
self.rack = rack
self.include_images = include_images
@staticmethod
def _add_gradient(drawing, id_, color):
gradient = drawing.linearGradient(
start=(0, 0),
end=(0, 25),
spreadMethod='repeat',
id_=id_,
gradientTransform='rotate(45, 0, 0)',
gradientUnits='userSpaceOnUse'
)
gradient.add_stop_color(offset='0%', color='#f7f7f7')
gradient.add_stop_color(offset='50%', color='#f7f7f7')
gradient.add_stop_color(offset='50%', color=color)
gradient.add_stop_color(offset='100%', color=color)
drawing.defs.add(gradient)
@staticmethod
def _setup_drawing(width, height):
drawing = svgwrite.Drawing(size=(width, height))
# add the stylesheet
with open('{}/css/rack_elevation.css'.format(settings.STATICFILES_DIRS[0])) as css_file:
drawing.defs.add(drawing.style(css_file.read()))
# add gradients
RackElevationSVG._add_gradient(drawing, 'reserved', '#c7c7ff')
RackElevationSVG._add_gradient(drawing, 'occupied', '#d7d7d7')
RackElevationSVG._add_gradient(drawing, 'blocked', '#ffc0c0')
return drawing
def _draw_device_front(self, drawing, device, start, end, text):
name = str(device)
if device.devicebay_count:
name += ' ({}/{})'.format(device.get_children().count(), device.devicebay_count)
color = device.device_role.color
link = drawing.add(
drawing.a(
href=reverse('dcim:device', kwargs={'pk': device.pk}),
target='_top',
fill='black'
)
)
link.set_desc('{}{} ({}U) {} {}'.format(
device.device_role, device.device_type.display_name,
device.device_type.u_height, device.asset_tag or '', device.serial or ''
))
link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
hex_color = '#{}'.format(foreground_color(color))
link.add(drawing.text(str(name), insert=text, fill=hex_color))
# Embed front device type image if one exists
if self.include_images and device.device_type.front_image:
url = device.device_type.front_image.url
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
image.fit(scale='slice')
link.add(image)
def _draw_device_rear(self, drawing, device, start, end, text):
rect = drawing.rect(start, end, class_="slot blocked")
rect.set_desc('{}{} ({}U) {} {}'.format(
device.device_role, device.device_type.display_name,
device.device_type.u_height, device.asset_tag or '', device.serial or ''
))
drawing.add(rect)
drawing.add(drawing.text(str(device), insert=text))
# Embed rear device type image if one exists
if self.include_images and device.device_type.rear_image:
url = device.device_type.rear_image.url
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
image.fit(scale='slice')
drawing.add(image)
@staticmethod
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
link = drawing.add(
drawing.a(
href='{}?{}'.format(
reverse('dcim:device_add'),
urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
),
target='_top'
)
)
if reservation:
link.set_desc('{}{} · {}'.format(
reservation.description, reservation.user, reservation.created
))
link.add(drawing.rect(start, end, class_=class_))
link.add(drawing.text("add device", insert=text, class_='add-device'))
def merge_elevations(self, face):
elevation = self.rack.get_rack_units(face=face, expand_devices=False)
if face == DeviceFaceChoices.FACE_REAR:
other_face = DeviceFaceChoices.FACE_FRONT
else:
other_face = DeviceFaceChoices.FACE_REAR
other = self.rack.get_rack_units(face=other_face)
unit_cursor = 0
for u in elevation:
o = other[unit_cursor]
if not u['device'] and o['device']:
u['device'] = o['device']
u['height'] = 1
unit_cursor += u.get('height', 1)
return elevation
def render(self, face, unit_width, unit_height, legend_width):
"""
Return an SVG document representing a rack elevation.
"""
drawing = self._setup_drawing(
unit_width + legend_width + RACK_ELEVATION_BORDER_WIDTH * 2,
unit_height * self.rack.u_height + RACK_ELEVATION_BORDER_WIDTH * 2
)
reserved_units = self.rack.get_reserved_units()
unit_cursor = 0
for ru in range(0, self.rack.u_height):
start_y = ru * unit_height
position_coordinates = (legend_width / 2, start_y + unit_height / 2 + RACK_ELEVATION_BORDER_WIDTH)
unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru
drawing.add(
drawing.text(str(unit), position_coordinates, class_="unit")
)
for unit in self.merge_elevations(face):
# Loop through all units in the elevation
device = unit['device']
height = unit.get('height', 1)
# Setup drawing coordinates
x_offset = legend_width + RACK_ELEVATION_BORDER_WIDTH
y_offset = unit_cursor * unit_height + RACK_ELEVATION_BORDER_WIDTH
end_y = unit_height * height
start_cordinates = (x_offset, y_offset)
end_cordinates = (unit_width, end_y)
text_cordinates = (x_offset + (unit_width / 2), y_offset + end_y / 2)
# Draw the device
if device and device.face == face:
self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates)
elif device and device.device_type.is_full_depth:
self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates)
else:
# Draw shallow devices, reservations, or empty units
class_ = 'slot'
reservation = reserved_units.get(unit["id"])
if device:
class_ += ' occupied'
if reservation:
class_ += ' reserved'
self._draw_empty(
drawing,
self.rack,
start_cordinates,
end_cordinates,
text_cordinates,
unit["id"],
face,
class_,
reservation
)
unit_cursor += height
# Wrap the drawing with a border
border_width = RACK_ELEVATION_BORDER_WIDTH
border_offset = RACK_ELEVATION_BORDER_WIDTH / 2
frame = drawing.rect(
insert=(legend_width + border_offset, border_offset),
size=(unit_width + border_width, self.rack.u_height * unit_height + border_width),
class_='rack'
)
drawing.add(frame)
return drawing

View File

@ -3,14 +3,24 @@ from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models from django.db import models
from netaddr import AddrFormatError, EUI, mac_unix_expanded from netaddr import AddrFormatError, EUI, mac_unix_expanded
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
class ASNField(models.BigIntegerField): class ASNField(models.BigIntegerField):
description = "32-bit ASN field" description = "32-bit ASN field"
default_validators = [ default_validators = [
MinValueValidator(1), MinValueValidator(BGP_ASN_MIN),
MaxValueValidator(4294967295), MaxValueValidator(BGP_ASN_MAX),
] ]
def formfield(self, **kwargs):
defaults = {
'min_value': BGP_ASN_MIN,
'max_value': BGP_ASN_MAX,
}
defaults.update(**kwargs)
return super().formfield(**defaults)
class mac_unix_expanded_uppercase(mac_unix_expanded): class mac_unix_expanded_uppercase(mac_unix_expanded):
word_fmt = '%.2X' word_fmt = '%.2X'
@ -22,7 +32,7 @@ class MACAddressField(models.Field):
def python_type(self): def python_type(self):
return EUI return EUI
def from_db_value(self, value, expression, connection, context): def from_db_value(self, value, expression, connection):
return self.to_python(value) return self.to_python(value)
def to_python(self, value): def to_python(self, value):

View File

@ -1,9 +1,8 @@
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets 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.constants import COLOR_CHOICES
from utilities.filters import ( from utilities.filters import (
@ -11,6 +10,7 @@ from utilities.filters import (
TagFilter, TreeNodeMultipleChoiceFilter, TagFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .choices import *
from .constants import * from .constants import *
from .models import ( from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@ -21,7 +21,46 @@ from .models import (
) )
class RegionFilter(NameSlugSearchFilterSet, CustomFieldFilterSet): __all__ = (
'CableFilterSet',
'ConsoleConnectionFilterSet',
'ConsolePortFilterSet',
'ConsolePortTemplateFilterSet',
'ConsoleServerPortFilterSet',
'ConsoleServerPortTemplateFilterSet',
'DeviceBayFilterSet',
'DeviceBayTemplateFilterSet',
'DeviceFilterSet',
'DeviceRoleFilterSet',
'DeviceTypeFilterSet',
'FrontPortFilterSet',
'FrontPortTemplateFilterSet',
'InterfaceConnectionFilterSet',
'InterfaceFilterSet',
'InterfaceTemplateFilterSet',
'InventoryItemFilterSet',
'ManufacturerFilterSet',
'PlatformFilterSet',
'PowerConnectionFilterSet',
'PowerFeedFilterSet',
'PowerOutletFilterSet',
'PowerOutletTemplateFilterSet',
'PowerPanelFilterSet',
'PowerPortFilterSet',
'PowerPortTemplateFilterSet',
'RackFilterSet',
'RackGroupFilterSet',
'RackReservationFilterSet',
'RackRoleFilterSet',
'RearPortFilterSet',
'RearPortTemplateFilterSet',
'RegionFilterSet',
'SiteFilterSet',
'VirtualChassisFilterSet',
)
class RegionFilterSet(NameSlugSearchFilterSet, CustomFieldFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Parent region (ID)', label='Parent region (ID)',
@ -38,7 +77,7 @@ class RegionFilter(NameSlugSearchFilterSet, CustomFieldFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -48,7 +87,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
label='Search', label='Search',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=SITE_STATUS_CHOICES, choices=SiteStatusChoices,
null_value=None null_value=None
) )
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
@ -92,7 +131,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class RackGroupFilter(NameSlugSearchFilterSet): class RackGroupFilterSet(NameSlugSearchFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region__in', field_name='site__region__in',
@ -120,14 +159,14 @@ class RackGroupFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class RackRoleFilter(NameSlugSearchFilterSet): class RackRoleFilterSet(NameSlugSearchFilterSet):
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'name', 'slug', 'color'] fields = ['id', 'name', 'slug', 'color']
class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -168,7 +207,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
label='Group', label='Group',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=RACK_STATUS_CHOICES, choices=RackStatusChoices,
null_value=None null_value=None
) )
role_id = django_filters.ModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
@ -205,7 +244,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
) )
class RackReservationFilter(TenancyFilterSet): class RackReservationFilterSet(TenancyFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -266,14 +305,14 @@ class RackReservationFilter(TenancyFilterSet):
) )
class ManufacturerFilter(NameSlugSearchFilterSet): class ManufacturerFilterSet(NameSlugSearchFilterSet):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): class DeviceTypeFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -316,6 +355,10 @@ class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
method='_pass_through_ports', method='_pass_through_ports',
label='Has pass-through ports', label='Has pass-through ports',
) )
device_bays = django_filters.BooleanFilter(
method='_device_bays',
label='Has device bays',
)
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:
@ -355,6 +398,9 @@ class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
rearport_templates__isnull=value rearport_templates__isnull=value
) )
def _device_bays(self, queryset, name, value):
return queryset.exclude(device_bay_templates__isnull=value)
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet): class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter( devicetype_id = django_filters.ModelMultipleChoiceFilter(
@ -364,70 +410,70 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
) )
class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet): class ConsolePortTemplateFilterSet(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'name'] fields = ['id', 'name', 'type']
class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet): class ConsoleServerPortTemplateFilterSet(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'name'] fields = ['id', 'name', 'type']
class PowerPortTemplateFilter(DeviceTypeComponentFilterSet): class PowerPortTemplateFilterSet(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'name', 'maximum_draw', 'allocated_draw'] fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet): class PowerOutletTemplateFilterSet(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'name', 'feed_leg'] fields = ['id', 'name', 'type', 'feed_leg']
class InterfaceTemplateFilter(DeviceTypeComponentFilterSet): class InterfaceTemplateFilterSet(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'name', 'type', 'mgmt_only'] fields = ['id', 'name', 'type', 'mgmt_only']
class FrontPortTemplateFilter(DeviceTypeComponentFilterSet): class FrontPortTemplateFilterSet(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class RearPortTemplateFilter(DeviceTypeComponentFilterSet): class RearPortTemplateFilterSet(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = ['id', 'name', 'type', 'positions'] fields = ['id', 'name', 'type', 'positions']
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet): class DeviceBayTemplateFilterSet(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ['id', 'name'] fields = ['id', 'name']
class DeviceRoleFilter(NameSlugSearchFilterSet): class DeviceRoleFilterSet(NameSlugSearchFilterSet):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role'] fields = ['id', 'name', 'slug', 'color', 'vm_role']
class PlatformFilter(NameSlugSearchFilterSet): class PlatformFilterSet(NameSlugSearchFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer', field_name='manufacturer',
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -445,7 +491,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver'] fields = ['id', 'name', 'slug', 'napalm_driver']
class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -532,7 +578,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter
label='Device model (slug)', label='Device model (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=DEVICE_STATUS_CHOICES, choices=DeviceStatusChoices,
null_value=None null_value=None
) )
is_full_depth = django_filters.BooleanFilter( is_full_depth = django_filters.BooleanFilter(
@ -583,6 +629,10 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter
method='_pass_through_ports', method='_pass_through_ports',
label='Has pass-through ports', label='Has pass-through ports',
) )
device_bays = django_filters.BooleanFilter(
method='_device_bays',
label='Has device bays',
)
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:
@ -636,17 +686,43 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter
rearports__isnull=value rearports__isnull=value
) )
def _device_bays(self, queryset, name, value):
return queryset.exclude(device_bays__isnull=value)
class DeviceComponentFilterSet(django_filters.FilterSet): class DeviceComponentFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
) )
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site name (slug)',
)
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
) )
device = django_filters.ModelChoiceFilter( device = django_filters.ModelMultipleChoiceFilter(
field_name='device__name',
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
label='Device (name)', label='Device (name)',
@ -662,7 +738,11 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
) )
class ConsolePortFilter(DeviceComponentFilterSet): class ConsolePortFilterSet(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@ -674,7 +754,11 @@ class ConsolePortFilter(DeviceComponentFilterSet):
fields = ['id', 'name', 'description', 'connection_status'] fields = ['id', 'name', 'description', 'connection_status']
class ConsoleServerPortFilter(DeviceComponentFilterSet): class ConsoleServerPortFilterSet(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@ -686,7 +770,11 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
fields = ['id', 'name', 'description', 'connection_status'] fields = ['id', 'name', 'description', 'connection_status']
class PowerPortFilter(DeviceComponentFilterSet): class PowerPortFilterSet(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PowerPortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@ -698,7 +786,11 @@ class PowerPortFilter(DeviceComponentFilterSet):
fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status'] fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status']
class PowerOutletFilter(DeviceComponentFilterSet): class PowerOutletFilterSet(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PowerOutletTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@ -710,14 +802,13 @@ class PowerOutletFilter(DeviceComponentFilterSet):
fields = ['id', 'name', 'feed_leg', 'description', 'connection_status'] fields = ['id', 'name', 'feed_leg', 'description', 'connection_status']
class InterfaceFilter(django_filters.FilterSet): class InterfaceFilterSet(DeviceComponentFilterSet):
"""
Not using DeviceComponentFilterSet for Interfaces because we need to check for VirtualChassis membership.
"""
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
) )
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
# members
device = MultiValueCharFilter( device = MultiValueCharFilter(
method='filter_device', method='filter_device',
field_name='name', field_name='name',
@ -753,7 +844,7 @@ class InterfaceFilter(django_filters.FilterSet):
label='Assigned VID' label='Assigned VID'
) )
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=IFACE_TYPE_CHOICES, choices=InterfaceTypeChoices,
null_value=None null_value=None
) )
@ -761,14 +852,6 @@ class InterfaceFilter(django_filters.FilterSet):
model = Interface model = Interface
fields = ['id', 'name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description'] fields = ['id', 'name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
).distinct()
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
try: try:
devices = Device.objects.filter(**{'{}__in'.format(name): value}) devices = Device.objects.filter(**{'{}__in'.format(name): value})
@ -817,7 +900,7 @@ class InterfaceFilter(django_filters.FilterSet):
}.get(value, queryset.none()) }.get(value, queryset.none())
class FrontPortFilter(DeviceComponentFilterSet): class FrontPortFilterSet(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@ -829,7 +912,7 @@ class FrontPortFilter(DeviceComponentFilterSet):
fields = ['id', 'name', 'type', 'description'] fields = ['id', 'name', 'type', 'description']
class RearPortFilter(DeviceComponentFilterSet): class RearPortFilterSet(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@ -841,14 +924,14 @@ class RearPortFilter(DeviceComponentFilterSet):
fields = ['id', 'name', 'type', 'positions', 'description'] fields = ['id', 'name', 'type', 'positions', 'description']
class DeviceBayFilter(DeviceComponentFilterSet): class DeviceBayFilterSet(DeviceComponentFilterSet):
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = ['id', 'name', 'description'] fields = ['id', 'name', 'description']
class InventoryItemFilter(DeviceComponentFilterSet): class InventoryItemFilterSet(DeviceComponentFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -919,7 +1002,7 @@ class InventoryItemFilter(DeviceComponentFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class VirtualChassisFilter(django_filters.FilterSet): class VirtualChassisFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -973,16 +1056,16 @@ class VirtualChassisFilter(django_filters.FilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class CableFilter(django_filters.FilterSet): class CableFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
) )
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=CABLE_TYPE_CHOICES choices=CableTypeChoices
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=CONNECTION_STATUS_CHOICES choices=CableStatusChoices
) )
color = django_filters.MultipleChoiceFilter( color = django_filters.MultipleChoiceFilter(
choices=COLOR_CHOICES choices=COLOR_CHOICES
@ -1010,6 +1093,14 @@ class CableFilter(django_filters.FilterSet):
method='filter_device', method='filter_device',
field_name='device__site__slug' field_name='device__site__slug'
) )
tenant_id = MultiValueNumberFilter(
method='filter_device',
field_name='device__tenant_id'
)
tenant = MultiValueNumberFilter(
method='filter_device',
field_name='device__tenant__slug'
)
class Meta: class Meta:
model = Cable model = Cable
@ -1028,7 +1119,7 @@ class CableFilter(django_filters.FilterSet):
return queryset return queryset
class ConsoleConnectionFilter(django_filters.FilterSet): class ConsoleConnectionFilterSet(django_filters.FilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1059,7 +1150,7 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
) )
class PowerConnectionFilter(django_filters.FilterSet): class PowerConnectionFilterSet(django_filters.FilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1090,7 +1181,7 @@ class PowerConnectionFilter(django_filters.FilterSet):
) )
class InterfaceConnectionFilter(django_filters.FilterSet): class InterfaceConnectionFilterSet(django_filters.FilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1124,7 +1215,7 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
) )
class PowerPanelFilter(django_filters.FilterSet): class PowerPanelFilterSet(django_filters.FilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -1173,7 +1264,7 @@ class PowerPanelFilter(django_filters.FilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): class PowerFeedFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

File diff suppressed because it is too large Load Diff

View File

@ -1,195 +0,0 @@
[
{
"model": "dcim.devicerole",
"pk": 1,
"fields": {
"name": "Console Server",
"slug": "console-server",
"color": "009688"
}
},
{
"model": "dcim.devicerole",
"pk": 2,
"fields": {
"name": "Core Switch",
"slug": "core-switch",
"color": "2196f3"
}
},
{
"model": "dcim.devicerole",
"pk": 3,
"fields": {
"name": "Distribution Switch",
"slug": "distribution-switch",
"color": "2196f3"
}
},
{
"model": "dcim.devicerole",
"pk": 4,
"fields": {
"name": "Access Switch",
"slug": "access-switch",
"color": "2196f3"
}
},
{
"model": "dcim.devicerole",
"pk": 5,
"fields": {
"name": "Management Switch",
"slug": "management-switch",
"color": "ff9800"
}
},
{
"model": "dcim.devicerole",
"pk": 6,
"fields": {
"name": "Firewall",
"slug": "firewall",
"color": "f44336"
}
},
{
"model": "dcim.devicerole",
"pk": 7,
"fields": {
"name": "Router",
"slug": "router",
"color": "9c27b0"
}
},
{
"model": "dcim.devicerole",
"pk": 8,
"fields": {
"name": "Server",
"slug": "server",
"color": "9e9e9e"
}
},
{
"model": "dcim.devicerole",
"pk": 9,
"fields": {
"name": "PDU",
"slug": "pdu",
"color": "607d8b"
}
},
{
"model": "dcim.manufacturer",
"pk": 1,
"fields": {
"name": "APC",
"slug": "apc"
}
},
{
"model": "dcim.manufacturer",
"pk": 2,
"fields": {
"name": "Cisco",
"slug": "cisco"
}
},
{
"model": "dcim.manufacturer",
"pk": 3,
"fields": {
"name": "Dell",
"slug": "dell"
}
},
{
"model": "dcim.manufacturer",
"pk": 4,
"fields": {
"name": "HP",
"slug": "hp"
}
},
{
"model": "dcim.manufacturer",
"pk": 5,
"fields": {
"name": "Juniper",
"slug": "juniper"
}
},
{
"model": "dcim.manufacturer",
"pk": 6,
"fields": {
"name": "Arista",
"slug": "arista"
}
},
{
"model": "dcim.manufacturer",
"pk": 7,
"fields": {
"name": "Opengear",
"slug": "opengear"
}
},
{
"model": "dcim.manufacturer",
"pk": 8,
"fields": {
"name": "Super Micro",
"slug": "super-micro"
}
},
{
"model": "dcim.platform",
"pk": 1,
"fields": {
"name": "Cisco IOS",
"slug": "cisco-ios"
}
},
{
"model": "dcim.platform",
"pk": 2,
"fields": {
"name": "Cisco NX-OS",
"slug": "cisco-nx-os"
}
},
{
"model": "dcim.platform",
"pk": 3,
"fields": {
"name": "Juniper Junos",
"slug": "juniper-junos"
}
},
{
"model": "dcim.platform",
"pk": 4,
"fields": {
"name": "Arista EOS",
"slug": "arista-eos"
}
},
{
"model": "dcim.platform",
"pk": 5,
"fields": {
"name": "Linux",
"slug": "linux"
}
},
{
"model": "dcim.platform",
"pk": 6,
"fields": {
"name": "Opengear",
"slug": "opengear"
}
}
]

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +0,0 @@
from django.db.models import Manager, QuerySet
from django.db.models.expressions import RawSQL
from .constants import NONCONNECTABLE_IFACE_TYPES
# Regular expressions for parsing Interface names
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9\.:]+)')"
SLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})/') AS integer), NULL)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?\d{{1,9}}/(\d{{1,9}})') AS integer), NULL)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{2}}(\d{{1,9}})') AS integer), NULL)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{3}}(\d{{1,9}})') AS integer), NULL)"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?(\d{{1,9}})([^/]|$)') AS integer)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
class InterfaceQuerySet(QuerySet):
def connectable(self):
"""
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
wireless).
"""
return self.exclude(type__in=NONCONNECTABLE_IFACE_TYPES)
class InterfaceManager(Manager):
def get_queryset(self):
"""
Naturally order interfaces by their type and numeric position. To order interfaces naturally, the `name` field
is split into eight distinct components: leading text (type), slot, subslot, position, subposition, ID, channel,
and virtual circuit:
{type}{slot or ID}/{subslot}/{position}/{subposition}:{channel}.{vc}
Components absent from the interface name are coalesced to zero or null. For example, an interface named
GigabitEthernet1/2/3 would be parsed as follows:
type = 'GigabitEthernet'
slot = 1
subslot = 2
position = 3
subposition = None
id = None
channel = 0
vc = 0
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
match any of the prescribed fields.
The `id` field is included to enforce deterministic ordering of interfaces in similar vein of other device
components.
"""
sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = [
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', 'pk'
]
fields = {
'_type': RawSQL(TYPE_RE.format(sql_col), []),
'_id': RawSQL(ID_RE.format(sql_col), []),
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
'_position': RawSQL(POSITION_RE.format(sql_col), []),
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
'_vc': RawSQL(VC_RE.format(sql_col), []),
}
return InterfaceQuerySet(self.model, using=self._db).annotate(**fields).order_by(*ordering)

View File

@ -1,259 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:06
import dcim.fields
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import utilities.fields
class Migration(migrations.Migration):
replaces = [('dcim', '0002_auto_20160622_1821'), ('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'), ('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', '0001_initial'),
('ipam', '0001_initial'),
('tenancy', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='device',
name='rack',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='device_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_port_templates', to='dcim.DeviceType'),
),
migrations.AddField(
model_name='consoleserverport',
name='device',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_ports', to='dcim.Device'),
),
migrations.AddField(
model_name='consoleporttemplate',
name='device_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_port_templates', to='dcim.DeviceType'),
),
migrations.AddField(
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=b'Console server port'),
),
migrations.AddField(
model_name='consoleport',
name='device',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_ports', to='dcim.Device'),
),
migrations.AlterUniqueTogether(
name='rackgroup',
unique_together=set([('site', 'name'), ('site', 'slug')]),
),
migrations.AlterUniqueTogether(
name='rack',
unique_together=set([('site', 'facility_id'), ('site', 'name')]),
),
migrations.AlterUniqueTogether(
name='powerporttemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='powerport',
unique_together=set([('device', 'name')]),
),
migrations.AlterUniqueTogether(
name='poweroutlettemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='poweroutlet',
unique_together=set([('device', 'name')]),
),
migrations.AlterUniqueTogether(
name='module',
unique_together=set([('device', 'parent', 'name')]),
),
migrations.AlterUniqueTogether(
name='interfacetemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AddField(
model_name='interface',
name='mac_address',
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'),
),
migrations.AlterUniqueTogether(
name='interface',
unique_together=set([('device', 'name')]),
),
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.AlterUniqueTogether(
name='devicetype',
unique_together=set([('manufacturer', 'slug'), ('manufacturer', 'model')]),
),
migrations.AlterUniqueTogether(
name='device',
unique_together=set([('rack', 'position', 'face')]),
),
migrations.AlterUniqueTogether(
name='consoleserverporttemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='consoleserverport',
unique_together=set([('device', 'name')]),
),
migrations.AlterUniqueTogether(
name='consoleporttemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='consoleport',
unique_together=set([('device', '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'],
},
),
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'],
},
),
migrations.AlterUniqueTogether(
name='devicebaytemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='devicebay',
unique_together=set([('device', 'name')]),
),
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.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'),
),
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.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', utilities.fields.ColorField(max_length=6)),
],
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.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.AlterField(
model_name='devicerole',
name='color',
field=utilities.fields.ColorField(max_length=6),
),
]

View File

@ -0,0 +1,101 @@
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

@ -0,0 +1,154 @@
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,12 +1,11 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:13
import dcim.fields
from django.conf import settings
import django.contrib.postgres.fields import django.contrib.postgres.fields
import django.core.validators import django.core.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import mptt.fields import mptt.fields
from django.conf import settings
from django.db import migrations, models
import dcim.fields
import utilities.fields import utilities.fields
@ -32,8 +31,8 @@ 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')] 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 = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('dcim', '0022_color_names_to_rgb'), ('dcim', '0022_color_names_to_rgb'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
@ -94,10 +93,15 @@ class Migration(migrations.Migration):
name='site', name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'), field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
), ),
migrations.AddField( migrations.AlterField(
model_name='interface', model_name='interface',
name='lag', name='form_factor',
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'), 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( migrations.CreateModel(
name='Region', name='Region',
@ -157,7 +161,17 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='device', model_name='device',
name='status', name='status',
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_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( migrations.AlterField(
model_name='consoleport', model_name='consoleport',
@ -199,6 +213,11 @@ class Migration(migrations.Migration):
name='serial', name='serial',
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), 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( migrations.AlterField(
model_name='devicebay', model_name='devicebay',
name='name', name='name',
@ -244,6 +263,16 @@ class Migration(migrations.Migration):
name='u_height', name='u_height',
field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'), 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( migrations.AlterField(
model_name='interface', model_name='interface',
name='mac_address', name='mac_address',
@ -259,6 +288,11 @@ class Migration(migrations.Migration):
name='connection_status', name='connection_status',
field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='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( migrations.AlterField(
model_name='interfacetemplate', model_name='interfacetemplate',
name='mgmt_only', name='mgmt_only',
@ -329,6 +363,16 @@ class Migration(migrations.Migration):
name='contact_email', name='contact_email',
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'), 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( migrations.AddField(
model_name='interface', model_name='interface',
name='enabled', name='enabled',

View File

@ -1,144 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:17
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import timezone_field.fields
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')]
dependencies = [
('dcim', '0043_device_component_name_lengths'),
('ipam', '0020_ipaddress_add_role_carp'),
('virtualization', '0001_virtualization'),
('tenancy', '0003_unicode_literals'),
]
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='interface',
name='untagged_vlan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
),
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(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')),
],
options={
'verbose_name_plural': 'virtual chassis',
'ordering': ['master'],
},
),
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=set([('rack', 'position', 'face'), ('virtual_chassis', 'vc_position')]),
),
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.CASCADE, related_name='platforms', to='dcim.Manufacturer'),
),
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),
),
]

View File

@ -0,0 +1,354 @@
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

@ -0,0 +1,124 @@
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'),
),
]

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