diff --git a/.dockerignore b/.dockerignore index c62d9ecd6..18722a3d1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,3 @@ -netbox scripts .tox .venv diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b01cb6dc8..38a6fd550 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -5,7 +5,9 @@ about: Report a reproducible bug in the current release of NetBox --- ### Environment -* Python version: -* NetBox version: +* Python version: +* NetBox version: @@ -14,5 +16,13 @@ about: Suggest an addition or modification to the NetBox documentation [ ] Deprecation [ ] Cleanup (formatting, typos, etc.) +### Area +[ ] Installation instructions +[ ] Configuration parameters +[ ] Functionality/features +[ ] REST API +[ ] Administration/development +[ ] Other + ### Proposed Changes diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ebe19d811..2f742d416 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -5,7 +5,9 @@ about: Propose a new NetBox feature or enhancement --- ### Environment -* Python version: -* NetBox version: +* Python version: +* NetBox version: ### Proposed Changes diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 000000000..e00f3f4db --- /dev/null +++ b/.github/lock.yml @@ -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 diff --git a/.github/stale.yml b/.github/stale.yml index 7c8d03f12..43401de8a 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -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 daysUntilStale: 14 + # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 + # Issues with these labels will never be considered stale exemptLabels: - "status: accepted" - "status: gathering feedback" - "status: blocked" + # Label to use when marking an issue as stale staleLabel: wontfix + # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md). + # Comment to post when closing a stale issue. Set to `false` to disable closeComment: > This issue has been automatically closed due to lack of activity. In an diff --git a/.gitignore b/.gitignore index cf7d50c98..7939c6f17 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ fabfile.py *.swp gunicorn_config.py +gunicorn.py +netbox.log +netbox.pid .DS_Store .vscode .venv diff --git a/.travis.yml b/.travis.yml index 29fa87b64..872121c21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ python: install: - pip install -r requirements.txt - pip install pycodestyle + - pip install coverage before_script: - psql --version - psql -U postgres -c 'SELECT version();' diff --git a/Dockerfile b/Dockerfile index 77c68ef0d..19e92b31f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM vaporio/python:3.6 +FROM vaporio/python:3.7 as builder RUN apt-get update -qy \ && apt-get install -y \ @@ -13,62 +13,49 @@ RUN apt-get update -qy \ ttf-ubuntu-font-family \ && 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 \ + greenlet \ + eventlet \ # napalm is used for gathering information from network devices napalm \ # ruamel is used in startup_scripts - ruamel.yaml \ -# pinning django to the version required by netbox -# adding it here, to install the correct version of -# django-rq - 'Django>=2.2,<2.3' \ -# django-rq is used for webhooks - django-rq + 'ruamel.yaml>=0.15,<0.16' \ +# django-storages was introduced in 2.7 and is optional + django-storages -ARG BRANCH -ARG ORG=vapor-ware +ARG NETBOX_PATH=. +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/) -ARG BUILD_VERSION -ARG BUILD_DATE -ARG VCS_REF - -LABEL maintainer="Vapor IO"\ - 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 - -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 +RUN apt-get update -qy \ + && apt-get install -y \ + libsasl2-dev \ + graphviz \ + libjpeg-dev \ + libffi-dev \ + libxml2-dev \ + libxslt1-dev \ + libldap2-dev \ + libpq-dev \ + ttf-ubuntu-font-family \ + && rm -rf /var/lib/apt/lists/* WORKDIR /opt -ARG URL=https://github.com/$ORG/netbox/archive/$BRANCH.tar.gz -RUN wget -q -O - "${URL}" | tar xz \ - && mv netbox* netbox +COPY --from=builder /install /usr/local + +ARG NETBOX_PATH=. +COPY ${NETBOX_PATH} /opt/netbox COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py 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/startup_scripts/ /opt/netbox/startup_scripts/ COPY docker/initializers/ /opt/netbox/initializers/ @@ -76,8 +63,39 @@ COPY docker/configuration/configuration.py /etc/netbox/config/configuration.py WORKDIR /opt/netbox/netbox +RUN mkdir -p static && chmod g+w static media + ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ] 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 diff --git a/README.md b/README.md index 478f37e5e..5167c53c4 100644 --- a/README.md +++ b/README.md @@ -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 rack elevation](docs/media/screenshot2.png "Rack elevation") +--- + ![Screenshot of prefix hierarchy](docs/media/screenshot3.png "Prefix hierarchy") # Installation diff --git a/base_requirements.txt b/base_requirements.txt index f0f6cfe38..ed42b6c08 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -22,14 +22,18 @@ django-filter # https://github.com/django-mptt/django-mptt django-mptt -# Django integration for RQ (Reqis queuing) -# https://github.com/rq/django-rq -django-rq +# Context managers for PostgreSQL advisory locks +# https://github.com/Xof/django-pglocks +django-pglocks # Prometheus metrics library for Django # https://github.com/korfuri/django-prometheus django-prometheus +# Django integration for RQ (Reqis queuing) +# https://github.com/rq/django-rq +django-rq + # Abstraction models for rendering and paginating HTML tables # https://github.com/jieter/django-tables2 django-tables2 @@ -54,9 +58,9 @@ djangorestframework # https://github.com/axnsan12/drf-yasg drf-yasg[validation] -# Python interface to the graphviz graph rendering utility -# https://github.com/xflr6/graphviz -graphviz +# Platform-agnostic template rendering engine +# https://github.com/pallets/jinja +Jinja2 # Simple markup language for rendering HTML # https://github.com/Python-Markdown/markdown @@ -82,3 +86,15 @@ py-gfm # Extensive cryptographic library (fork of pycrypto) # https://github.com/Legrandin/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 diff --git a/contrib/gunicorn.py b/contrib/gunicorn.py new file mode 100644 index 000000000..363dbc2ff --- /dev/null +++ b/contrib/gunicorn.py @@ -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 diff --git a/contrib/netbox-rq.service b/contrib/netbox-rq.service new file mode 100644 index 000000000..7a300a195 --- /dev/null +++ b/contrib/netbox-rq.service @@ -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 diff --git a/contrib/netbox.service b/contrib/netbox.service new file mode 100644 index 000000000..3cc9069c6 --- /dev/null +++ b/contrib/netbox.service @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 14658c461..5a7d420b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,40 +1,70 @@ -version: '3' +version: '3.4' 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: - image: "postgres:9.6" + image: postgres:11-alpine environment: POSTGRES_PASSWORD: "12345" POSTGRES_DB: netbox - ports: - - 5432:5432 + volumes: + - netbox-postgres-data:/var/lib/postgresql/data redis: - image: "redis" - nginx: - image: "nginx" + image: redis:5-alpine volumes: - - "static:/opt/netbox/netbox/static" - - "./docker/nginx.conf:/etc/nginx/nginx.conf" - ports: - - 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" + - netbox-redis-data:/data + redis-cache: + image: redis:5-alpine 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 diff --git a/docker/configuration/configuration.py b/docker/configuration/configuration.py index d1f38a664..af121d9ed 100644 --- a/docker/configuration/configuration.py +++ b/docker/configuration/configuration.py @@ -37,6 +37,10 @@ DATABASE = { # PostgreSQL password 'HOST': os.environ.get('DB_HOST', 'localhost'), # Database server '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. @@ -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 = { - 'HOST': os.environ.get('REDIS_HOST', 'localhost'), - 'PORT': int(os.environ.get('REDIS_PORT', 6379)), - 'PASSWORD': os.environ.get('REDIS_PASSWORD', read_secret('redis_password')), - 'DATABASE': os.environ.get('REDIS_DATABASE', '0'), - 'CACHE_DATABASE': os.environ.get('REDIS_CACHE_DATABASE', '1'), - 'DEFAULT_TIMEOUT': os.environ.get('REDIS_TIMEOUT', '300'), - 'SSL': os.environ.get('REDIS_SSL', 'False').lower() == 'true', + 'webhooks': { + 'HOST': os.environ.get('REDIS_HOST', 'localhost'), + 'PORT': int(os.environ.get('REDIS_PORT', 6379)), + 'PASSWORD': os.environ.get('REDIS_PASSWORD', read_secret('redis_password')), + 'DATABASE': int(os.environ.get('REDIS_DATABASE', 0)), + 'DEFAULT_TIMEOUT': int(os.environ.get('REDIS_TIMEOUT', 300)), + '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. 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 = 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: # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date DATE_FORMAT = os.environ.get('DATE_FORMAT', 'N j, Y') diff --git a/docker/configuration/ldap_config.py b/docker/configuration/ldap_config.py index 39fc894d7..ba2067c42 100644 --- a/docker/configuration/ldap_config.py +++ b/docker/configuration/ldap_config.py @@ -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' # 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_GROUP_CACHE_TIMEOUT = int(os.environ.get('AUTH_LDAP_GROUP_CACHE_TIMEOUT', 3600)) +AUTH_LDAP_CACHE_TIMEOUT = int(os.environ.get('AUTH_LDAP_CACHE_TIMEOUT', 3600)) # Populate the Django user from the LDAP directory. AUTH_LDAP_USER_ATTR_MAP = { diff --git a/docker/env/netbox.env b/docker/env/netbox.env new file mode 100644 index 000000000..f61466fc9 --- /dev/null +++ b/docker/env/netbox.env @@ -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 diff --git a/docker/nginx.conf b/docker/nginx.conf index 8e3abd950..3b78a9f91 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,3 +1,4 @@ +daemon off; worker_processes 1; error_log /dev/stderr info; @@ -17,7 +18,7 @@ http { client_max_body_size 10M; server { - listen 80; + listen 8080; access_log off; location /static/ { diff --git a/docker/startup_scripts/020_custom_fields-required.py b/docker/startup_scripts/020_custom_fields-required.py index 76a32bbf9..2f6ba72c1 100644 --- a/docker/startup_scripts/020_custom_fields-required.py +++ b/docker/startup_scripts/020_custom_fields-required.py @@ -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 ruamel.yaml import YAML from pathlib import Path 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): import importlib from django.contrib.contenttypes.models import ContentType @@ -42,12 +32,6 @@ with file.open('r') as stream: if cf_details.get('description', 0): 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): custom_field.label = cf_details['label'] @@ -58,7 +42,7 @@ with file.open('r') as stream: custom_field.required = cf_details['required'] 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): custom_field.weight = cf_details['weight'] diff --git a/docker/startup_scripts/080_racks.py b/docker/startup_scripts/080_racks.py index 05bca1082..ed7713d8a 100644 --- a/docker/startup_scripts/080_racks.py +++ b/docker/startup_scripts/080_racks.py @@ -1,7 +1,6 @@ from dcim.models import Site, RackRole, Rack, RackGroup from tenancy.models import Tenant from extras.models import CustomField, CustomFieldValue -from dcim.constants import RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES from ruamel.yaml import YAML from pathlib import Path import sys @@ -41,14 +40,6 @@ with file.open('r') as stream: 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) if created: diff --git a/docker/startup_scripts/110_devices.py b/docker/startup_scripts/110_devices.py index 584b5c255..4217549d7 100644 --- a/docker/startup_scripts/110_devices.py +++ b/docker/startup_scripts/110_devices.py @@ -1,5 +1,4 @@ from dcim.models import Site, Rack, DeviceRole, DeviceType, Device, Platform -from dcim.constants import RACK_FACE_CHOICES from ipam.models import IPAddress from virtualization.models import Cluster from tenancy.models import Tenant @@ -26,6 +25,7 @@ with file.open('r') as stream: optional_assocs = { 'tenant': (Tenant, 'name'), 'platform': (Platform, 'name'), + 'rack': (Rack, 'name'), 'cluster': (Cluster, 'name'), 'primary_ip4': (IPAddress, 'address'), 'primary_ip6': (IPAddress, 'address') @@ -48,14 +48,6 @@ with file.open('r') as stream: 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) if created: diff --git a/docker/startup_scripts/120_interfaces.py b/docker/startup_scripts/120_interfaces.py index c1849afe7..906f52335 100644 --- a/docker/startup_scripts/120_interfaces.py +++ b/docker/startup_scripts/120_interfaces.py @@ -1,5 +1,4 @@ from dcim.models import Interface, Device, DeviceRole -from dcim.constants import IFACE_TYPE_KEYSTONE from ruamel.yaml import YAML from pathlib import Path @@ -31,6 +30,6 @@ with file.open('r') as stream: 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: print("🔗 Created interface {} for {}".format(interface.name, locker.name)) diff --git a/docker/startup_scripts/210_vlans.py b/docker/startup_scripts/210_vlans.py new file mode 100644 index 000000000..ab6bd2ba7 --- /dev/null +++ b/docker/startup_scripts/210_vlans.py @@ -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) diff --git a/docker/startup_scripts/220_prefixes.py b/docker/startup_scripts/220_prefixes.py new file mode 100644 index 000000000..d13578a06 --- /dev/null +++ b/docker/startup_scripts/220_prefixes.py @@ -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) diff --git a/docker/startup_scripts/250_dcim_interfaces.py b/docker/startup_scripts/250_dcim_interfaces.py new file mode 100644 index 000000000..ec30b5c6e --- /dev/null +++ b/docker/startup_scripts/250_dcim_interfaces.py @@ -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) diff --git a/docker/startup_scripts/260_ip_addresses.py b/docker/startup_scripts/260_ip_addresses.py new file mode 100644 index 000000000..d109a36b9 --- /dev/null +++ b/docker/startup_scripts/260_ip_addresses.py @@ -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) diff --git a/docs/additional-features/custom-scripts.md b/docs/additional-features/custom-scripts.md index c4dffb4b9..0904f8c82 100644 --- a/docs/additional-features/custom-scripts.md +++ b/docs/additional-features/custom-scripts.md @@ -27,11 +27,17 @@ class MyScript(Script): var2 = IntegerVar(...) 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. @@ -124,7 +130,7 @@ Arbitrary text of any length. Renders as multi-line text input field. Stored a numeric integer. Options include: -* `min_value:` - Minimum value +* `min_value` - Minimum value * `max_value` - Maximum value ### 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. +### 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 -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 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 +* `description` - A brief description of the field +* `label` - The name of the form field * `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 @@ -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 dcim.constants import * +from dcim.choices import DeviceStatusChoices, SiteStatusChoices from dcim.models import Device, DeviceRole, DeviceType, Site from extras.scripts import * @@ -210,13 +228,13 @@ class NewBranchScript(Script): ) ) - def run(self, data): + def run(self, data, commit): # Create the new site site = Site( name=data['site_name'], slug=slugify(data['site_name']), - status=SITE_STATUS_PLANNED + status=SiteStatusChoices.STATUS_PLANNED ) site.save() self.log_success("Created new site: {}".format(site)) @@ -228,7 +246,7 @@ class NewBranchScript(Script): device_type=data['switch_model'], name='{}-switch{}'.format(site.slug, i), site=site, - status=DEVICE_STATUS_PLANNED, + status=DeviceStatusChoices.STATUS_PLANNED, device_role=switch_role ) switch.save() diff --git a/docs/additional-features/graphs.md b/docs/additional-features/graphs.md index b20a6b424..264b7f1b7 100644 --- a/docs/additional-features/graphs.md +++ b/docs/additional-features/graphs.md @@ -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`. * **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 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: diff --git a/docs/additional-features/napalm.md b/docs/additional-features/napalm.md new file mode 100644 index 000000000..c8e8b8b3a --- /dev/null +++ b/docs/additional-features/napalm.md @@ -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" +``` diff --git a/docs/additional-features/reports.md b/docs/additional-features/reports.md index fc1e89221..6deddc140 100644 --- a/docs/additional-features/reports.md +++ b/docs/additional-features/reports.md @@ -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. ``` -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 extras.reports import Report @@ -43,7 +44,8 @@ class DeviceConnectionsReport(Report): def test_console_connection(self): # 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: self.log_failure( console_port.device, @@ -60,7 +62,7 @@ class DeviceConnectionsReport(Report): def test_power_connections(self): # 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 for power_port in PowerPort.objects.filter(device=device): if power_port.connected_endpoint is not None: diff --git a/docs/additional-features/topology-maps.md b/docs/additional-features/topology-maps.md deleted file mode 100644 index 21bbe404d..000000000 --- a/docs/additional-features/topology-maps.md +++ /dev/null @@ -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. diff --git a/docs/additional-features/webhooks.md b/docs/additional-features/webhooks.md index 9a02449f8..310e67bf5 100644 --- a/docs/additional-features/webhooks.md +++ b/docs/additional-features/webhooks.md @@ -1,61 +1,73 @@ # 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: +* 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 { "event": "created", - "timestamp": "2019-10-12 12:51:29.746944", - "username": "admin", + "timestamp": "2020-02-25 15:10:26.010582+00:00", "model": "site", - "request_id": "43d8e212-94c7-4f67-b544-0dcde4fc0f43", + "username": "jstretch", + "request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a", "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 -{ - "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 - } -} -``` +When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under Django RQ > Queues. -A request is considered successful if the response 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. - -## 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/. +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. diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 89532d4b7..cbe01728c 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -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 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) * 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 @@ -127,7 +149,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*'] --- -# ENFORCE_GLOBAL_UNIQUE +## ENFORCE_GLOBAL_UNIQUE 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 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 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). diff --git a/docs/configuration/required-settings.md b/docs/configuration/required-settings.md index 92b2fbfb8..e86b2810a 100644 --- a/docs/configuration/required-settings.md +++ b/docs/configuration/required-settings.md @@ -21,11 +21,11 @@ NetBox requires access to a PostgreSQL database service to store data. This serv * `PASSWORD` - PostgreSQL password * `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) -* `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: -``` +```python DATABASE = { 'NAME': 'netbox', # Database name '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](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching -functionality (as well as other planned features). +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) * `PORT` - TCP port of the Redis service; leave blank for default port (6379) * `PASSWORD` - Redis password (if set) -* `DATABASE` - Numeric database ID for webhooks -* `CACHE_DATABASE` - Numeric database ID for caching +* `DATABASE` - Numeric database ID * `DEFAULT_TIMEOUT` - Connection timeout in seconds * `SSL` - Use SSL connection to Redis Example: -``` +```python REDIS = { - 'HOST': 'localhost', - 'PORT': 6379, - 'PASSWORD': '', - 'DATABASE': 0, - 'CACHE_DATABASE': 1, - 'DEFAULT_TIMEOUT': 300, - 'SSL': False, + '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: - If you were using these settings in a prior release with webhooks, the `DATABASE` setting remains the same but - an additional `CACHE_DATABASE` setting has been added with a default value of 1 to support the caching backend. The - `DATABASE` setting will be renamed in a future release of NetBox to better relay the meaning of the setting. +!!! note + If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have + changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary + +!!! 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. --- diff --git a/docs/core-functionality/power.md b/docs/core-functionality/power.md new file mode 100644 index 000000000..1eda1aa00 --- /dev/null +++ b/docs/core-functionality/power.md @@ -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 | + +--------+ +--------+ +``` diff --git a/docs/core-functionality/secrets.md b/docs/core-functionality/secrets.md index 36b232648..515dd8d07 100644 --- a/docs/core-functionality/secrets.md +++ b/docs/core-functionality/secrets.md @@ -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. +## 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 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. diff --git a/docs/core-functionality/sites-and-racks.md b/docs/core-functionality/sites-and-racks.md index bf3c473fd..f86e24b3e 100644 --- a/docs/core-functionality/sites-and-racks.md +++ b/docs/core-functionality/sites-and-racks.md @@ -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. +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 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. diff --git a/docs/development/extending-models.md b/docs/development/extending-models.md index 0070c5545..dd44bb2ab 100644 --- a/docs/development/extending-models.md +++ b/docs/development/extending-models.md @@ -69,6 +69,14 @@ If the new field will be included in the object list view, add a column to the m Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated. -### 11. Adjust API and model tests +### 11. Create/extend test cases -Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. +Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including: + +* API serializer/view tests +* Filter tests +* Form tests +* Model tests +* View tests + +Be diligent to ensure all of the relevant test suites are adapted or extended as necessary to test any new functionality. diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 008647b8e..60be6bf61 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -33,6 +33,10 @@ Update the following static libraries to their most recent stable release: * jQuery * 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 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`. diff --git a/docs/development/squashing-migrations.md b/docs/development/squashing-migrations.md new file mode 100644 index 000000000..bc0c0548f --- /dev/null +++ b/docs/development/squashing-migrations.md @@ -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 +``` + +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. diff --git a/docs/development/style-guide.md b/docs/development/style-guide.md index 4c490eebf..53b2215b3 100644 --- a/docs/development/style-guide.md +++ b/docs/development/style-guide.md @@ -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. -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 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. +* 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. * 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`. + +## 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. diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 5e9c98c5c..376a62ae2 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -4,7 +4,7 @@ NetBox requires a PostgreSQL database to store data. This can be hosted locally The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. !!! warning - NetBox v2.2 and later requires PostgreSQL 9.4 or higher. + NetBox requires PostgreSQL 9.4 or higher. # Installation diff --git a/docs/installation/2-netbox.md b/docs/installation/2-netbox.md index 6d2706eb0..cbe2c70c0 100644 --- a/docs/installation/2-netbox.md +++ b/docs/installation/2-netbox.md @@ -5,14 +5,14 @@ This section of the documentation discusses installing and configuring the NetBo **Ubuntu** ```no-highlight -# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev 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** ```no-highlight # 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 # 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 ``` +## Remote File Storage (Optional) + +By default, NetBox will use the local filesystem to storage uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired backend](../../configuration/optional-settings/#storage_backend) in `configuration.py`. + +```no-highlight +# pip3 install django-storages +``` + # Configuration Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`. @@ -139,13 +147,22 @@ Redis is a in-memory key-value store required as part of the NetBox installation ```python REDIS = { - 'HOST': 'localhost', - 'PORT': 6379, - 'PASSWORD': '', - 'DATABASE': 0, - 'CACHE_DATABASE': 1, - 'DEFAULT_TIMEOUT': 300, - 'SSL': False, + '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, + } } ``` @@ -195,27 +212,7 @@ Superuser created successfully. ```no-highlight # python3 manage.py collectstatic --no-input -You have requested to collect static files at the destination -location as specified in your settings: - - /opt/netbox/netbox/static - -This will overwrite existing files! -Are you sure you want to do this? - -Type 'yes' to continue, or 'no' to cancel: yes -``` - -# Load Initial Data (Optional) - -NetBox ships with some initial data to help you get started: RIR definitions, common devices roles, etc. You can delete any seed data that you don't want to keep. - -!!! note - This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch. - -```no-highlight -# python3 manage.py loaddata initial_data -Installed 43 object(s) from 4 fixture(s) +959 static files copied to '/opt/netbox/netbox/static'. ``` # Test the Application @@ -237,3 +234,11 @@ Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on !!! warning If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected. + +Note that the initial UI will be locked down for non-authenticated users. + +![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) diff --git a/docs/installation/3-http-daemon.md b/docs/installation/3-http-daemon.md index 9c29fc979..4225f8413 100644 --- a/docs/installation/3-http-daemon.md +++ b/docs/installation/3-http-daemon.md @@ -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 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 / { 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-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). +!!! 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 Install gunicorn: @@ -107,47 +110,54 @@ 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 -command = '/usr/bin/gunicorn' -pythonpath = '/opt/netbox/netbox' -bind = '127.0.0.1:8001' -workers = 3 -user = 'www-data' -max_requests = 5000 -max_requests_jitter = 500 +# cd /opt/netbox +# cp contrib/gunicorn.py /opt/netbox/gunicorn.py ``` -# 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 -# 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 -[program:netbox] -command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi -directory = /opt/netbox/netbox/ -user = www-data - -[program:netbox-rqworker] -command = python3 /opt/netbox/netbox/manage.py rqworker -directory = /opt/netbox/netbox/ -user = www-data +# systemctl daemon-reload +# systemctl start netbox.service +# systemctl start netbox-rq.service +# systemctl enable netbox.service +# systemctl enable netbox-rq.service ``` -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 - 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. diff --git a/docs/installation/4-ldap.md b/docs/installation/4-ldap.md index a41400808..953d3cb28 100644 --- a/docs/installation/4-ldap.md +++ b/docs/installation/4-ldap.md @@ -110,8 +110,8 @@ AUTH_LDAP_USER_FLAGS_BY_GROUP = { AUTH_LDAP_FIND_GROUP_PERMS = True # Cache groups for one hour to reduce LDAP traffic -AUTH_LDAP_CACHE_GROUPS = True -AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 +AUTH_LDAP_CACHE_TIMEOUT = 3600 + ``` * `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in. diff --git a/docs/installation/index.md b/docs/installation/index.md index 54daa62e3..59631bf7a 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -4,7 +4,7 @@ The following sections detail how to set up a new instance of NetBox: 1. [PostgreSQL database](1-postgresql.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) # 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). 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. diff --git a/docs/installation/migrating-to-systemd.md b/docs/installation/migrating-to-systemd.md new file mode 100644 index 000000000..f5fcb7598 --- /dev/null +++ b/docs/installation/migrating-to-systemd.md @@ -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. diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 85af66536..e5cf93a28 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -84,14 +84,12 @@ This script: # 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 -# sudo supervisorctl restart netbox +# sudo systemctl restart netbox +# sudo systemctl restart netbox-rq ``` -If using webhooks, also restart the Redis worker: - -```no-highlight -# sudo supervisorctl restart netbox-rqworker -``` +!!! 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. diff --git a/docs/media/installation/netbox_ui_admin.png b/docs/media/installation/netbox_ui_admin.png new file mode 100644 index 000000000..bde4947d5 Binary files /dev/null and b/docs/media/installation/netbox_ui_admin.png differ diff --git a/docs/media/installation/netbox_ui_guest.png b/docs/media/installation/netbox_ui_guest.png new file mode 100644 index 000000000..a20a5467a Binary files /dev/null and b/docs/media/installation/netbox_ui_guest.png differ diff --git a/docs/media/screenshot1.png b/docs/media/screenshot1.png index 3939199d9..e25a5ed93 100644 Binary files a/docs/media/screenshot1.png and b/docs/media/screenshot1.png differ diff --git a/docs/media/screenshot2.png b/docs/media/screenshot2.png index 3a73fe048..bce0a3f5d 100644 Binary files a/docs/media/screenshot2.png and b/docs/media/screenshot2.png differ diff --git a/docs/media/screenshot3.png b/docs/media/screenshot3.png index d2acb9773..6cf67d8f7 100644 Binary files a/docs/media/screenshot3.png and b/docs/media/screenshot3.png differ diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md index 10b4ba75c..e44a306fe 120000 --- a/docs/release-notes/index.md +++ b/docs/release-notes/index.md @@ -1 +1 @@ -version-2.6.md \ No newline at end of file +version-2.7.md \ No newline at end of file diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 2bf55d857..9fd258b0f 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -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) ## Bug Fixes diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md new file mode 100644 index 000000000..f9557bf34 --- /dev/null +++ b/docs/release-notes/version-2.7.md @@ -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': '', + 'AWS_SECRET_ACCESS_KEY': '', + '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//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//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//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 + `.`; e.g. `dcim.site`. +* ipam.Role: Added field `description` +* secrets.SecretRole: Added field `description` +* virtualization.Cluster: Added field `tenant` diff --git a/mkdocs.yml b/mkdocs.yml index cc44921b6..4ba91dfe5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,6 +12,7 @@ pages: - 4. LDAP (Optional): 'installation/4-ldap.md' - Upgrading NetBox: 'installation/upgrading.md' - Migrating to Python3: 'installation/migrating-to-python3.md' + - Migrating to systemd: 'installation/migrating-to-systemd.md' - Configuration: - Configuring NetBox: 'configuration/index.md' - Required Settings: 'configuration/required-settings.md' @@ -24,6 +25,7 @@ pages: - Virtual Machines: 'core-functionality/virtual-machines.md' - Services: 'core-functionality/services.md' - Circuits: 'core-functionality/circuits.md' + - Power: 'core-functionality/power.md' - Secrets: 'core-functionality/secrets.md' - Tenancy: 'core-functionality/tenancy.md' - Additional Features: @@ -35,10 +37,10 @@ pages: - Custom Scripts: 'additional-features/custom-scripts.md' - Export Templates: 'additional-features/export-templates.md' - Graphs: 'additional-features/graphs.md' + - NAPALM: 'additional-features/napalm.md' - Prometheus Metrics: 'additional-features/prometheus-metrics.md' - Reports: 'additional-features/reports.md' - Tags: 'additional-features/tags.md' - - Topology Maps: 'additional-features/topology-maps.md' - Webhooks: 'additional-features/webhooks.md' - Administration: - Replicating NetBox: 'administration/replicating-netbox.md' @@ -54,7 +56,9 @@ pages: - Utility Views: 'development/utility-views.md' - Extending Models: 'development/extending-models.md' - Release Checklist: 'development/release-checklist.md' + - Squashing Migrations: 'development/squashing-migrations.md' - Release Notes: + - Version 2.7: 'release-notes/version-2.7.md' - Version 2.6: 'release-notes/version-2.6.md' - Version 2.5: 'release-notes/version-2.5.md' - Version 2.4: 'release-notes/version-2.4.md' diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 39a0b6b26..6bac48a59 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,13 +1,13 @@ from rest_framework import serializers 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 dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer +from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer from dcim.api.serializers import ConnectedEndpointSerializer from extras.api.customfields import CustomFieldModelSerializer from tenancy.api.nested_serializers import NestedTenantSerializer -from utilities.api import ChoiceField, ValidatedModelSerializer +from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer from .nested_serializers import * @@ -36,21 +36,33 @@ class CircuitTypeSerializer(ValidatedModelSerializer): class Meta: 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): provider = NestedProviderSerializer() - status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False) + status = ChoiceField(choices=CircuitStatusChoices, required=False) type = NestedCircuitTypeSerializer() tenant = NestedTenantSerializer(required=False, allow_null=True) + termination_a = CircuitCircuitTerminationSerializer(read_only=True) + termination_z = CircuitCircuitTerminationSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = Circuit fields = [ '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', ] diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index b9d1b439b..cd3015d0a 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -15,15 +15,15 @@ router = routers.DefaultRouter() router.APIRootView = CircuitsRootView # Field choices -router.register(r'_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice') # Providers -router.register(r'providers', views.ProviderViewSet) +router.register('providers', views.ProviderViewSet) # Circuits -router.register(r'circuit-types', views.CircuitTypeViewSet) -router.register(r'circuits', views.CircuitViewSet) -router.register(r'circuit-terminations', views.CircuitTerminationViewSet) +router.register('circuit-types', views.CircuitTypeViewSet) +router.register('circuits', views.CircuitViewSet) +router.register('circuit-terminations', views.CircuitTerminationViewSet) app_name = 'circuits-api' urlpatterns = router.urls diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 65b0db14b..75f7e0e3e 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -7,7 +7,7 @@ from circuits import filters from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet -from extras.models import Graph, GRAPH_TYPE_PROVIDER +from extras.models import Graph from utilities.api import FieldChoicesViewSet, ModelViewSet from . import serializers @@ -18,8 +18,8 @@ from . import serializers class CircuitsFieldChoicesViewSet(FieldChoicesViewSet): fields = ( - (Circuit, ['status']), - (CircuitTermination, ['term_side']), + (serializers.CircuitSerializer, ['status']), + (serializers.CircuitTerminationSerializer, ['term_side']), ) @@ -32,7 +32,7 @@ class ProviderViewSet(CustomFieldModelViewSet): circuit_count=Count('circuits') ) serializer_class = serializers.ProviderSerializer - filterset_class = filters.ProviderFilter + filterset_class = filters.ProviderFilterSet @action(detail=True) def graphs(self, request, pk): @@ -40,7 +40,7 @@ class ProviderViewSet(CustomFieldModelViewSet): A convenience method for rendering graphs for a particular provider. """ provider = get_object_or_404(Provider, pk=pk) - queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER) + queryset = Graph.objects.filter(type__model='provider') serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider}) return Response(serializer.data) @@ -54,7 +54,7 @@ class CircuitTypeViewSet(ModelViewSet): circuit_count=Count('circuits') ) serializer_class = serializers.CircuitTypeSerializer - filterset_class = filters.CircuitTypeFilter + filterset_class = filters.CircuitTypeFilterSet # @@ -62,9 +62,11 @@ class CircuitTypeViewSet(ModelViewSet): # 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 - filterset_class = filters.CircuitFilter + filterset_class = filters.CircuitFilterSet # @@ -76,4 +78,4 @@ class CircuitTerminationViewSet(ModelViewSet): 'circuit', 'site', 'connected_endpoint__device', 'cable' ) serializer_class = serializers.CircuitTerminationSerializer - filterset_class = filters.CircuitTerminationFilter + filterset_class = filters.CircuitTerminationFilterSet diff --git a/netbox/circuits/choices.py b/netbox/circuits/choices.py new file mode 100644 index 000000000..94a765d11 --- /dev/null +++ b/netbox/circuits/choices.py @@ -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') + ) diff --git a/netbox/circuits/constants.py b/netbox/circuits/constants.py deleted file mode 100644 index 9e180e655..000000000 --- a/netbox/circuits/constants.py +++ /dev/null @@ -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'), -) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 0ac5ec170..c27ffb8d7 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -3,13 +3,20 @@ from django.db.models import Q from dcim.models import Region, Site 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 .constants import * +from .choices import * from .models import Circuit, CircuitTermination, CircuitType, Provider +__all__ = ( + 'CircuitFilterSet', + 'CircuitTerminationFilterSet', + 'CircuitTypeFilterSet', + 'ProviderFilterSet', +) -class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): + +class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -58,14 +65,14 @@ class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): ) -class CircuitTypeFilter(NameSlugSearchFilterSet): +class CircuitTypeFilterSet(NameSlugSearchFilterSet): class Meta: model = CircuitType fields = ['id', 'name', 'slug'] -class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): +class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -95,7 +102,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter label='Circuit type (slug)', ) status = django_filters.MultipleChoiceFilter( - choices=CIRCUIT_STATUS_CHOICES, + choices=CircuitStatusChoices, null_value=None ) site_id = django_filters.ModelMultipleChoiceFilter( @@ -139,7 +146,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter ).distinct() -class CircuitTerminationFilter(django_filters.FilterSet): +class CircuitTerminationFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/circuits/fixtures/initial_data.json b/netbox/circuits/fixtures/initial_data.json deleted file mode 100644 index c918bbeea..000000000 --- a/netbox/circuits/fixtures/initial_data.json +++ /dev/null @@ -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" - } -} -] diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 4a5c06a6e..0b0378a7a 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -2,14 +2,17 @@ from django import forms from taggit.forms import TagField 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.models import Tenant from utilities.forms import ( - APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, - DatePicker, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple + APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker, + DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2, + StaticSelect2Multiple, TagFilterField, ) -from .constants import * +from .choices import CircuitStatusChoices from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -17,7 +20,7 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider # Providers # -class ProviderForm(BootstrapMixin, CustomFieldForm): +class ProviderForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() comments = CommentField() tags = TagField( @@ -46,7 +49,7 @@ class ProviderForm(BootstrapMixin, CustomFieldForm): } -class ProviderCSVForm(forms.ModelForm): +class ProviderCSVForm(CustomFieldModelCSVForm): slug = SlugField() class Meta: @@ -89,7 +92,8 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdi label='Admin contact' ) comments = CommentField( - widget=SmallTextarea() + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -104,7 +108,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) - region = FilterChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -116,9 +120,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): } ) ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", @@ -128,6 +133,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='ASN' ) + tag = TagFilterField(model) # @@ -140,7 +146,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm): class Meta: model = CircuitType fields = [ - 'name', 'slug', + 'name', 'slug', 'description', ] @@ -159,7 +165,19 @@ class CircuitTypeCSVForm(forms.ModelForm): # 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() tags = TagField( required=False @@ -176,18 +194,12 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): 'commit_rate': "Committed rate", } widgets = { - 'provider': APISelect( - api_url="/api/circuits/providers/" - ), - 'type': APISelect( - api_url="/api/circuits/circuit-types/" - ), 'status': StaticSelect2(), 'install_date': DatePicker(), } -class CircuitCSVForm(forms.ModelForm): +class CircuitCSVForm(CustomFieldModelCSVForm): provider = forms.ModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', @@ -205,7 +217,7 @@ class CircuitCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=CIRCUIT_STATUS_CHOICES, + choices=CircuitStatusChoices, required=False, help_text='Operational status' ) @@ -231,14 +243,14 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput ) - type = forms.ModelChoiceField( + type = DynamicModelChoiceField( queryset=CircuitType.objects.all(), required=False, widget=APISelect( api_url="/api/circuits/circuit-types/" ) ) - provider = forms.ModelChoiceField( + provider = DynamicModelChoiceField( queryset=Provider.objects.all(), required=False, widget=APISelect( @@ -246,12 +258,12 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit ) ) status = forms.ChoiceField( - choices=add_blank_choice(CIRCUIT_STATUS_CHOICES), + choices=add_blank_choice(CircuitStatusChoices), required=False, initial='', widget=StaticSelect2() ) - tenant = forms.ModelChoiceField( + tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False, widget=APISelect( @@ -286,28 +298,30 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm required=False, label='Search' ) - type = FilterChoiceField( + type = DynamicModelMultipleChoiceField( queryset=CircuitType.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/circuits/circuit-types/", value_field="slug", ) ) - provider = FilterChoiceField( + provider = DynamicModelMultipleChoiceField( queryset=Provider.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/circuits/providers/", value_field="slug", ) ) status = forms.MultipleChoiceField( - choices=CIRCUIT_STATUS_CHOICES, + choices=CircuitStatusChoices, required=False, widget=StaticSelect2Multiple() ) - region = forms.ModelMultipleChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -319,9 +333,10 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm } ) ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", @@ -332,6 +347,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm min_value=0, label='Commit rate (Kbps)' ) + tag = TagFilterField(model) # diff --git a/netbox/circuits/migrations/0001_initial_squashed_0010_circuit_status.py b/netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py similarity index 58% rename from netbox/circuits/migrations/0001_initial_squashed_0010_circuit_status.py rename to netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py index 3fcec7933..4eec30667 100644 --- a/netbox/circuits/migrations/0001_initial_squashed_0010_circuit_status.py +++ b/netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py @@ -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 +from django.db import migrations, models + +import dcim.fields + + +def circuits_to_terms(apps, schema_editor): + Circuit = apps.get_model('circuits', 'Circuit') + CircuitTermination = apps.get_model('circuits', 'CircuitTermination') + for c in Circuit.objects.all(): + CircuitTermination( + circuit=c, + term_side=b'A', + site=c.site, + interface=c.interface, + port_speed=c.port_speed, + upstream_speed=c.upstream_speed, + xconnect_id=c.xconnect_id, + pp_info=c.pp_info, + ).save() class Migration(migrations.Migration): - replaces = [('circuits', '0001_initial'), ('circuits', '0002_auto_20160622_1821'), ('circuits', '0003_provider_32bit_asn_support'), ('circuits', '0004_circuit_add_tenant'), ('circuits', '0005_circuit_add_upstream_speed'), ('circuits', '0006_terminations'), ('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 = [ + ('tenancy', '0001_initial'), ('dcim', '0001_initial'), ('dcim', '0022_color_names_to_rgb'), - ('tenancy', '0001_initial'), ] 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( name='CircuitType', fields=[ @@ -46,49 +42,93 @@ class Migration(migrations.Migration): 'ordering': ['name'], }, ), + migrations.CreateModel( + name='Provider', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateField(auto_now_add=True)), + ('last_updated', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', models.SlugField(unique=True)), + ('asn', dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN')), + ('account', models.CharField(blank=True, max_length=30, verbose_name=b'Account number')), + ('portal_url', models.URLField(blank=True, verbose_name=b'Portal')), + ('noc_contact', models.TextField(blank=True, verbose_name=b'NOC contact')), + ('admin_contact', models.TextField(blank=True, verbose_name=b'Admin contact')), + ('comments', models.TextField(blank=True)), + ], + options={ + 'ordering': ['name'], + }, + ), migrations.CreateModel( name='Circuit', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateField(auto_now_add=True)), ('last_updated', models.DateTimeField(auto_now=True)), - ('cid', models.CharField(max_length=50, verbose_name='Circuit ID')), - ('install_date', models.DateField(blank=True, null=True, verbose_name='Date installed')), - ('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')), + ('cid', models.CharField(max_length=50, verbose_name=b'Circuit ID')), + ('install_date', models.DateField(blank=True, null=True, verbose_name=b'Date installed')), + ('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')), + ('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'Commit rate (Kbps)')), + ('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')), + ('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')), ('comments', models.TextField(blank=True)), + ('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit', to='dcim.Interface')), ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.Provider')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='dcim.Site')), ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.CircuitType')), ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant')), - ('description', models.CharField(blank=True, max_length=100)), - ('status', models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1)) + ('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')), ], options={ 'ordering': ['provider', 'cid'], + 'unique_together': {('provider', 'cid')}, }, ), - migrations.AlterUniqueTogether( - name='circuit', - unique_together=set([('provider', 'cid')]), - ), migrations.CreateModel( name='CircuitTermination', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('term_side', models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination')), - ('port_speed', models.PositiveIntegerField(verbose_name='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)')), - ('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID')), - ('pp_info', models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)')), + ('term_side', models.CharField(choices=[(b'A', b'A'), (b'Z', b'Z')], max_length=1, verbose_name='Termination')), + ('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')), + ('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')), + ('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')), + ('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')), ('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.Circuit')), - ('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.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')), ], options={ 'ordering': ['circuit', 'term_side'], + 'unique_together': {('circuit', 'term_side')}, }, ), - migrations.AlterUniqueTogether( - name='circuittermination', - unique_together=set([('circuit', 'term_side')]), + migrations.RunPython( + code=circuits_to_terms, + ), + migrations.RemoveField( + model_name='circuit', + name='interface', + ), + migrations.RemoveField( + model_name='circuit', + name='port_speed', + ), + migrations.RemoveField( + model_name='circuit', + name='pp_info', + ), + migrations.RemoveField( + model_name='circuit', + name='site', + ), + migrations.RemoveField( + model_name='circuit', + name='upstream_speed', + ), + migrations.RemoveField( + model_name='circuit', + name='xconnect_id', ), ] diff --git a/netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py b/netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py new file mode 100644 index 000000000..5bcd863a4 --- /dev/null +++ b/netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py @@ -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), + ), + ] diff --git a/netbox/circuits/migrations/0013_cables.py b/netbox/circuits/migrations/0013_cables.py index 4e9125a99..ec0284be0 100644 --- a/netbox/circuits/migrations/0013_cables.py +++ b/netbox/circuits/migrations/0013_cables.py @@ -3,7 +3,7 @@ import sys from django.db import migrations, models import django.db.models.deletion -from dcim.constants import CONNECTION_STATUS_CONNECTED +CONNECTION_STATUS_CONNECTED = True def circuit_terminations_to_cables(apps, schema_editor): diff --git a/netbox/circuits/migrations/0016_3569_circuit_fields.py b/netbox/circuits/migrations/0016_3569_circuit_fields.py new file mode 100644 index 000000000..a65f72d61 --- /dev/null +++ b/netbox/circuits/migrations/0016_3569_circuit_fields.py @@ -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 + ), + + ] diff --git a/netbox/circuits/migrations/0017_circuittype_description.py b/netbox/circuits/migrations/0017_circuittype_description.py new file mode 100644 index 000000000..4cb5591dd --- /dev/null +++ b/netbox/circuits/migrations/0017_circuittype_description.py @@ -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), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 8cf18617c..59f6e2004 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -3,13 +3,21 @@ from django.db import models from django.urls import reverse 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.models import CableTermination from extras.models import CustomFieldModel, ObjectChange, TaggedItem from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object -from .constants import * +from .choices import * + + +__all__ = ( + 'Circuit', + 'CircuitTermination', + 'CircuitType', + 'Provider', +) class Provider(ChangeLoggedModel, CustomFieldModel): @@ -57,7 +65,12 @@ class Provider(ChangeLoggedModel, CustomFieldModel): 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: ordering = ['name'] @@ -93,8 +106,12 @@ class CircuitType(ChangeLoggedModel): slug = models.SlugField( unique=True ) + description = models.CharField( + max_length=100, + blank=True, + ) - csv_headers = ['name', 'slug'] + csv_headers = ['name', 'slug', 'description'] class Meta: ordering = ['name'] @@ -109,6 +126,7 @@ class CircuitType(ChangeLoggedModel): return ( self.name, self.slug, + self.description, ) @@ -132,9 +150,10 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): on_delete=models.PROTECT, related_name='circuits' ) - status = models.PositiveSmallIntegerField( - choices=CIRCUIT_STATUS_CHOICES, - default=CIRCUIT_STATUS_ACTIVE + status = models.CharField( + max_length=50, + choices=CircuitStatusChoices, + default=CircuitStatusChoices.STATUS_ACTIVE ) tenant = models.ForeignKey( to='tenancy.Tenant', @@ -170,6 +189,18 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): csv_headers = [ 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', ] + clone_fields = [ + 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', + ] + + STATUS_CLASS_MAP = { + CircuitStatusChoices.STATUS_DEPROVISIONING: 'warning', + CircuitStatusChoices.STATUS_ACTIVE: 'success', + CircuitStatusChoices.STATUS_PLANNED: 'info', + CircuitStatusChoices.STATUS_PROVISIONING: 'primary', + CircuitStatusChoices.STATUS_OFFLINE: 'danger', + CircuitStatusChoices.STATUS_DECOMMISSIONED: 'default', + } class Meta: ordering = ['provider', 'cid'] @@ -195,7 +226,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): ) def get_status_class(self): - return STATUS_CLASSES[self.status] + return self.STATUS_CLASS_MAP.get(self.status) def _get_termination(self, side): for ct in self.terminations.all(): @@ -220,7 +251,7 @@ class CircuitTermination(CableTermination): ) term_side = models.CharField( max_length=1, - choices=TERM_SIDE_CHOICES, + choices=CircuitTerminationSideChoices, verbose_name='Termination' ) site = models.ForeignKey( diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index d67abdd1a..a425b3ace 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -6,7 +6,7 @@ from utilities.tables import BaseTable, ToggleColumn from .models import Circuit, CircuitType, Provider CIRCUITTYPE_ACTIONS = """ - + {% if perms.circuit.change_circuittype %} @@ -50,12 +50,14 @@ class CircuitTypeTable(BaseTable): name = tables.LinkColumn() circuit_count = tables.Column(verbose_name='Circuits') actions = tables.TemplateColumn( - template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' + template_code=CIRCUITTYPE_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' ) class Meta(BaseTable.Meta): model = CircuitType - fields = ('pk', 'name', 'circuit_count', 'slug', 'actions') + fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') # diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index e53c2c402..b1b6d9e14 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -1,12 +1,35 @@ +from django.contrib.contenttypes.models import ContentType from django.urls import reverse from rest_framework import status -from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z +from circuits.choices import * from circuits.models import Circuit, CircuitTermination, CircuitType, Provider -from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site -from extras.constants import GRAPH_TYPE_PROVIDER +from dcim.models import Site 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): @@ -28,16 +51,20 @@ class ProviderTest(APITestCase): def test_get_provider_graphs(self): + provider_ct = ContentType.objects.get(app_label='circuits', model='provider') self.graph1 = Graph.objects.create( - type=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' ) 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' ) 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' ) @@ -250,7 +277,7 @@ class CircuitTest(APITestCase): 'cid': 'TEST0004', 'provider': self.provider1.pk, 'type': self.circuittype1.pk, - 'status': CIRCUIT_STATUS_ACTIVE, + 'status': CircuitStatusChoices.STATUS_ACTIVE, } url = reverse('circuits-api:circuit-list') @@ -270,19 +297,19 @@ class CircuitTest(APITestCase): 'cid': 'TEST0004', 'provider': self.provider1.pk, 'type': self.circuittype1.pk, - 'status': CIRCUIT_STATUS_ACTIVE, + 'status': CircuitStatusChoices.STATUS_ACTIVE, }, { 'cid': 'TEST0005', 'provider': self.provider1.pk, 'type': self.circuittype1.pk, - 'status': CIRCUIT_STATUS_ACTIVE, + 'status': CircuitStatusChoices.STATUS_ACTIVE, }, { 'cid': 'TEST0006', 'provider': self.provider1.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.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype) 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( - 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( - 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( - 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): @@ -366,7 +405,7 @@ class CircuitTerminationTest(APITestCase): data = { 'circuit': self.circuit3.pk, - 'term_side': TERM_SIDE_A, + 'term_side': CircuitTerminationSideChoices.SIDE_A, 'site': self.site1.pk, 'port_speed': 1000000, } @@ -385,12 +424,15 @@ class CircuitTerminationTest(APITestCase): def test_update_circuittermination(self): circuittermination5 = CircuitTermination.objects.create( - circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 + circuit=self.circuit3, + term_side=CircuitTerminationSideChoices.SIDE_A, + site=self.site1, + port_speed=1000000 ) data = { 'circuit': self.circuit3.pk, - 'term_side': TERM_SIDE_Z, + 'term_side': CircuitTerminationSideChoices.SIDE_Z, 'site': self.site2.pk, 'port_speed': 1000000, } diff --git a/netbox/circuits/tests/test_filters.py b/netbox/circuits/tests/test_filters.py new file mode 100644 index 000000000..46c2bacbe --- /dev/null +++ b/netbox/circuits/tests/test_filters.py @@ -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) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index cb0ea0a32..9cc7af6ae 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -1,18 +1,15 @@ -import urllib.parse - -from django.test import Client, TestCase -from django.urls import reverse +import datetime +from circuits.choices import * 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): - user = create_test_user(permissions=['circuits.view_provider']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): Provider.objects.bulk_create([ 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), ]) - def test_provider_list(self): - - url = reverse('circuits:provider_list') - params = { - "q": "test", + cls.form_data = { + 'name': 'Provider X', + 'slug': 'provider-x', + 'asn': 65123, + '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))) - self.assertEqual(response.status_code, 200) + cls.csv_data = ( + "name,slug", + "Provider 4,provider-4", + "Provider 5,provider-5", + "Provider 6,provider-6", + ) - def test_provider(self): - - provider = Provider.objects.first() - response = self.client.get(provider.get_absolute_url()) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'asn': 65009, + 'account': '5678', + 'portal_url': 'http://example.com/portal2', + '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): - user = create_test_user(permissions=['circuits.view_circuittype']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): CircuitType.objects.bulk_create([ 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'), ]) - def test_circuittype_list(self): - - url = reverse('circuits:circuittype_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - -class CircuitTestCase(TestCase): - - def setUp(self): - 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, + cls.form_data = { + 'name': 'Circuit Type X', + 'slug': 'circuit-type-x', + 'description': 'A new circuit type', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + cls.csv_data = ( + "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() - response = self.client.get(circuit.get_absolute_url()) - self.assertEqual(response.status_code, 200) +class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = Circuit + + @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', + + } diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index c142a831a..72d9720df 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -9,42 +9,42 @@ app_name = 'circuits' urlpatterns = [ # Providers - path(r'providers/', views.ProviderListView.as_view(), name='provider_list'), - path(r'providers/add/', views.ProviderCreateView.as_view(), name='provider_add'), - path(r'providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'), - path(r'providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), - path(r'providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), - path(r'providers//', views.ProviderView.as_view(), name='provider'), - path(r'providers//edit/', views.ProviderEditView.as_view(), name='provider_edit'), - path(r'providers//delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), - path(r'providers//changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), + path('providers/', views.ProviderListView.as_view(), name='provider_list'), + path('providers/add/', views.ProviderCreateView.as_view(), name='provider_add'), + path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'), + path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), + path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), + path('providers//', views.ProviderView.as_view(), name='provider'), + path('providers//edit/', views.ProviderEditView.as_view(), name='provider_edit'), + path('providers//delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), + path('providers//changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), # Circuit types - path(r'circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), - path(r'circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), - path(r'circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), - path(r'circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), - path(r'circuit-types//edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), - path(r'circuit-types//changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), + path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), + path('circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), + path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), + path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), + path('circuit-types//edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), + path('circuit-types//changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), # Circuits - path(r'circuits/', views.CircuitListView.as_view(), name='circuit_list'), - path(r'circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'), - path(r'circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'), - path(r'circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), - path(r'circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), - path(r'circuits//', views.CircuitView.as_view(), name='circuit'), - path(r'circuits//edit/', views.CircuitEditView.as_view(), name='circuit_edit'), - path(r'circuits//delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), - path(r'circuits//changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), - path(r'circuits//terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'), + path('circuits/', views.CircuitListView.as_view(), name='circuit_list'), + path('circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'), + path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'), + path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), + path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), + path('circuits//', views.CircuitView.as_view(), name='circuit'), + path('circuits//edit/', views.CircuitEditView.as_view(), name='circuit_edit'), + path('circuits//delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), + path('circuits//changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), + path('circuits//terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'), # Circuit terminations - path(r'circuits//terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), - path(r'circuit-terminations//edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), - path(r'circuit-terminations//delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), - path(r'circuit-terminations//connect//', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), - path(r'circuit-terminations//trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), + path('circuits//terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), + path('circuit-terminations//edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), + path('circuit-terminations//delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), + path('circuit-terminations//connect//', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), + path('circuit-terminations//trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), ] diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 655b714d7..ba873f23f 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin @@ -5,14 +6,16 @@ from django.db import transaction from django.db.models import Count, OuterRef, Subquery from django.shortcuts import get_object_or_404, redirect, render from django.views.generic import View +from django_tables2 import RequestConfig -from extras.models import Graph, GRAPH_TYPE_PROVIDER +from extras.models import Graph from utilities.forms import ConfirmationForm +from utilities.paginator import EnhancedPaginator from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables -from .constants import TERM_SIDE_A, TERM_SIDE_Z +from .choices import CircuitTerminationSideChoices from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -23,10 +26,9 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider class ProviderListView(PermissionRequiredMixin, ObjectListView): permission_required = 'circuits.view_provider' queryset = Provider.objects.annotate(count_circuits=Count('circuits')) - filter = filters.ProviderFilter - filter_form = forms.ProviderFilterForm + filterset = filters.ProviderFilterSet + filterset_form = forms.ProviderFilterForm table = tables.ProviderDetailTable - template_name = 'circuits/provider_list.html' class ProviderView(PermissionRequiredMixin, View): @@ -36,11 +38,20 @@ class ProviderView(PermissionRequiredMixin, View): provider = get_object_or_404(Provider, slug=slug) circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site') - show_graphs = Graph.objects.filter(type=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', { 'provider': provider, - 'circuits': circuits, + 'circuits_table': circuits_table, 'show_graphs': show_graphs, }) @@ -73,7 +84,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView): class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'circuits.change_provider' queryset = Provider.objects.all() - filter = filters.ProviderFilter + filterset = filters.ProviderFilterSet table = tables.ProviderTable form = forms.ProviderBulkEditForm default_return_url = 'circuits:provider_list' @@ -82,7 +93,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_provider' queryset = Provider.objects.all() - filter = filters.ProviderFilter + filterset = filters.ProviderFilterSet table = tables.ProviderTable default_return_url = 'circuits:provider_list' @@ -95,7 +106,6 @@ class CircuitTypeListView(PermissionRequiredMixin, ObjectListView): permission_required = 'circuits.view_circuittype' queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')) table = tables.CircuitTypeTable - template_name = 'circuits/circuittype_list.html' class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView): @@ -136,10 +146,9 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView): a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]), z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]), ) - filter = filters.CircuitFilter - filter_form = forms.CircuitFilterForm + filterset = filters.CircuitFilterSet + filterset_form = forms.CircuitFilterForm table = tables.CircuitTable - template_name = 'circuits/circuit_list.html' class CircuitView(PermissionRequiredMixin, View): @@ -151,12 +160,12 @@ class CircuitView(PermissionRequiredMixin, View): termination_a = CircuitTermination.objects.prefetch_related( 'site__region', 'connected_endpoint__device' ).filter( - circuit=circuit, term_side=TERM_SIDE_A + circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A ).first() termination_z = CircuitTermination.objects.prefetch_related( 'site__region', 'connected_endpoint__device' ).filter( - circuit=circuit, term_side=TERM_SIDE_Z + circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z ).first() return render(request, 'circuits/circuit.html', { @@ -194,7 +203,7 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView): class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'circuits.change_circuit' queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site') - filter = filters.CircuitFilter + filterset = filters.CircuitFilterSet table = tables.CircuitTable form = forms.CircuitBulkEditForm default_return_url = 'circuits:circuit_list' @@ -203,7 +212,7 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_circuit' queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site') - filter = filters.CircuitFilter + filterset = filters.CircuitFilterSet table = tables.CircuitTable default_return_url = 'circuits:circuit_list' @@ -212,8 +221,12 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): def circuit_terminations_swap(request, pk): circuit = get_object_or_404(Circuit, pk=pk) - termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first() - termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first() + termination_a = CircuitTermination.objects.filter( + circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A + ).first() + termination_z = CircuitTermination.objects.filter( + circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z + ).first() if not termination_a and not termination_z: messages.error(request, "No terminations have been defined for circuit {}.".format(circuit)) return redirect('circuits:circuit', pk=circuit.pk) diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index a44e7227a..5afca0664 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -1,10 +1,11 @@ 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 ( Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, - Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, Rack, RackGroup, RackRole, - RearPort, RearPortTemplate, Region, Site, VirtualChassis, + Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, PowerPortTemplate, Rack, + RackGroup, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) from utilities.api import ChoiceField, WritableNestedSerializer @@ -25,6 +26,7 @@ __all__ = [ 'NestedPowerOutletSerializer', 'NestedPowerPanelSerializer', 'NestedPowerPortSerializer', + 'NestedPowerPortTemplateSerializer', 'NestedRackGroupSerializer', 'NestedRackRoleSerializer', 'NestedRackSerializer', @@ -111,6 +113,14 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer): 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): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail') @@ -203,7 +213,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer): device = NestedDeviceSerializer(read_only=True) url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) - type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) + type = ChoiceField(choices=InterfaceTypeChoices) class Meta: model = Interface diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index f3e9c3ad1..f2d2fdb00 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -4,6 +4,7 @@ from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField +from dcim.choices import * from dcim.constants import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -67,7 +68,7 @@ class RegionSerializer(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) tenant = NestedTenantSerializer(required=False, allow_null=True) time_zone = TimeZoneField(required=False) @@ -107,18 +108,18 @@ class RackRoleSerializer(ValidatedModelSerializer): class Meta: model = RackRole - fields = ['id', 'name', 'slug', 'color', 'rack_count'] + fields = ['id', 'name', 'slug', 'color', 'description', 'rack_count'] class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): site = NestedSiteSerializer() group = NestedRackGroupSerializer(required=False, allow_null=True, default=None) tenant = NestedTenantSerializer(required=False, allow_null=True) - status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False) + status = ChoiceField(choices=RackStatusChoices, required=False) role = NestedRackRoleSerializer(required=False, allow_null=True) - type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True) - width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False) - outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False) + type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False) + width = ChoiceField(choices=RackWidthChoices, required=False) + outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False) tags = TagListSerializerField(required=False) device_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) name = serializers.CharField(read_only=True) - face = serializers.IntegerField(read_only=True) + face = ChoiceField(choices=DeviceFaceChoices, read_only=True) device = NestedDeviceSerializer(read_only=True) @@ -170,6 +171,42 @@ class RackReservationSerializer(ValidatedModelSerializer): 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 # @@ -186,7 +223,7 @@ class ManufacturerSerializer(ValidatedModelSerializer): class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): 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) device_count = serializers.IntegerField(read_only=True) @@ -194,64 +231,83 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): model = DeviceType fields = [ '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): device_type = NestedDeviceTypeSerializer() + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) class Meta: model = ConsolePortTemplate - fields = ['id', 'device_type', 'name'] + fields = ['id', 'device_type', 'name', 'type'] class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) class Meta: model = ConsoleServerPortTemplate - fields = ['id', 'device_type', 'name'] + fields = ['id', 'device_type', 'name', 'type'] class PowerPortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() + type = ChoiceField( + choices=PowerPortTypeChoices, + allow_blank=True, + required=False + ) class Meta: model = PowerPortTemplate - fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw'] + fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw'] class PowerOutletTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() - power_port = PowerPortTemplateSerializer( + type = ChoiceField( + choices=PowerOutletTypeChoices, + allow_blank=True, + required=False + ) + power_port = NestedPowerPortTemplateSerializer( required=False ) feed_leg = ChoiceField( - choices=POWERFEED_LEG_CHOICES, - required=False, - allow_null=True + choices=PowerOutletFeedLegChoices, + allow_blank=True, + required=False ) class Meta: model = PowerOutletTemplate - fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg'] + fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg'] class InterfaceTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() - type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) - # TODO: Remove in v2.7 (backward-compatibility for form_factor) - form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) + type = ChoiceField(choices=InterfaceTypeChoices) class Meta: model = InterfaceTemplate - fields = ['id', 'device_type', 'name', 'type', 'form_factor', 'mgmt_only'] + fields = ['id', 'device_type', 'name', 'type', 'mgmt_only'] class RearPortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() - type = ChoiceField(choices=PORT_TYPE_CHOICES) + type = ChoiceField(choices=PortTypeChoices) class Meta: model = RearPortTemplate @@ -260,7 +316,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer): class FrontPortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() - type = ChoiceField(choices=PORT_TYPE_CHOICES) + type = ChoiceField(choices=PortTypeChoices) rear_port = NestedRearPortTemplateSerializer() class Meta: @@ -286,7 +342,9 @@ class DeviceRoleSerializer(ValidatedModelSerializer): class Meta: 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): @@ -309,8 +367,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer): platform = NestedPlatformSerializer(required=False, allow_null=True) site = NestedSiteSerializer() rack = NestedRackSerializer(required=False, allow_null=True) - face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True) - status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False) + face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False) + status = ChoiceField(choices=DeviceStatusChoices, required=False) primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = 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() +class DeviceNAPALMSerializer(serializers.Serializer): + method = serializers.DictField() + + class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = ConsoleServerPort fields = [ - 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', - 'cable', 'tags', + 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', + 'connection_status', 'cable', 'tags', ] class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = ConsolePort fields = [ - 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', - 'cable', 'tags', + 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', + 'connection_status', 'cable', 'tags', ] class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField( + choices=PowerOutletTypeChoices, + allow_blank=True, + required=False + ) power_port = NestedPowerPortSerializer( required=False ) feed_leg = ChoiceField( - choices=POWERFEED_LEG_CHOICES, - required=False, - allow_null=True + choices=PowerOutletFeedLegChoices, + allow_blank=True, + required=False ) cable = NestedCableSerializer( read_only=True @@ -416,31 +493,34 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = PowerOutlet 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', ] class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField( + choices=PowerPortTypeChoices, + allow_blank=True, + required=False + ) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = PowerPort 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', ] class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() - type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) - # TODO: Remove in v2.7 (backward-compatibility for form_factor) - form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) + type = ChoiceField(choices=InterfaceTypeChoices) 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) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), @@ -454,9 +534,9 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = Interface fields = [ - 'id', 'device', 'name', 'type', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', - 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', - 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', + 'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', + 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan', + 'tagged_vlans', 'tags', 'count_ipaddresses', ] # TODO: This validation should be handled by Interface.clean() @@ -482,7 +562,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer): device = NestedDeviceSerializer() - type = ChoiceField(choices=PORT_TYPE_CHOICES) + type = ChoiceField(choices=PortTypeChoices) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) @@ -504,7 +584,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer): class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer): device = NestedDeviceSerializer() - type = ChoiceField(choices=PORT_TYPE_CHOICES) + type = ChoiceField(choices=PortTypeChoices) rear_port = FrontPortRearPortSerializer() cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) @@ -549,15 +629,15 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer): class CableSerializer(ValidatedModelSerializer): termination_a_type = ContentTypeField( - queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES) + queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) ) 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_b = serializers.SerializerMethodField(read_only=True) - status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) - length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True) + status = ChoiceField(choices=CableStatusChoices, required=False) + length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False) class Meta: model = Cable @@ -662,20 +742,20 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer): default=None ) type = ChoiceField( - choices=POWERFEED_TYPE_CHOICES, - default=POWERFEED_TYPE_PRIMARY + choices=PowerFeedTypeChoices, + default=PowerFeedTypeChoices.TYPE_PRIMARY ) status = ChoiceField( - choices=POWERFEED_STATUS_CHOICES, - default=POWERFEED_STATUS_ACTIVE + choices=PowerFeedStatusChoices, + default=PowerFeedStatusChoices.STATUS_ACTIVE ) supply = ChoiceField( - choices=POWERFEED_SUPPLY_CHOICES, - default=POWERFEED_SUPPLY_AC + choices=PowerFeedSupplyChoices, + default=PowerFeedSupplyChoices.SUPPLY_AC ) phase = ChoiceField( - choices=POWERFEED_PHASE_CHOICES, - default=POWERFEED_PHASE_SINGLE + choices=PowerFeedPhaseChoices, + default=PowerFeedPhaseChoices.PHASE_SINGLE ) tags = TagListSerializerField( required=False diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index fd55d9b05..5a915becc 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -15,65 +15,65 @@ router = routers.DefaultRouter() router.APIRootView = DCIMRootView # Field choices -router.register(r'_choices', views.DCIMFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.DCIMFieldChoicesViewSet, basename='field-choice') # Sites -router.register(r'regions', views.RegionViewSet) -router.register(r'sites', views.SiteViewSet) +router.register('regions', views.RegionViewSet) +router.register('sites', views.SiteViewSet) # Racks -router.register(r'rack-groups', views.RackGroupViewSet) -router.register(r'rack-roles', views.RackRoleViewSet) -router.register(r'racks', views.RackViewSet) -router.register(r'rack-reservations', views.RackReservationViewSet) +router.register('rack-groups', views.RackGroupViewSet) +router.register('rack-roles', views.RackRoleViewSet) +router.register('racks', views.RackViewSet) +router.register('rack-reservations', views.RackReservationViewSet) # Device types -router.register(r'manufacturers', views.ManufacturerViewSet) -router.register(r'device-types', views.DeviceTypeViewSet) +router.register('manufacturers', views.ManufacturerViewSet) +router.register('device-types', views.DeviceTypeViewSet) # Device type components -router.register(r'console-port-templates', views.ConsolePortTemplateViewSet) -router.register(r'console-server-port-templates', views.ConsoleServerPortTemplateViewSet) -router.register(r'power-port-templates', views.PowerPortTemplateViewSet) -router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet) -router.register(r'interface-templates', views.InterfaceTemplateViewSet) -router.register(r'front-port-templates', views.FrontPortTemplateViewSet) -router.register(r'rear-port-templates', views.RearPortTemplateViewSet) -router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet) +router.register('console-port-templates', views.ConsolePortTemplateViewSet) +router.register('console-server-port-templates', views.ConsoleServerPortTemplateViewSet) +router.register('power-port-templates', views.PowerPortTemplateViewSet) +router.register('power-outlet-templates', views.PowerOutletTemplateViewSet) +router.register('interface-templates', views.InterfaceTemplateViewSet) +router.register('front-port-templates', views.FrontPortTemplateViewSet) +router.register('rear-port-templates', views.RearPortTemplateViewSet) +router.register('device-bay-templates', views.DeviceBayTemplateViewSet) # Devices -router.register(r'device-roles', views.DeviceRoleViewSet) -router.register(r'platforms', views.PlatformViewSet) -router.register(r'devices', views.DeviceViewSet) +router.register('device-roles', views.DeviceRoleViewSet) +router.register('platforms', views.PlatformViewSet) +router.register('devices', views.DeviceViewSet) # Device components -router.register(r'console-ports', views.ConsolePortViewSet) -router.register(r'console-server-ports', views.ConsoleServerPortViewSet) -router.register(r'power-ports', views.PowerPortViewSet) -router.register(r'power-outlets', views.PowerOutletViewSet) -router.register(r'interfaces', views.InterfaceViewSet) -router.register(r'front-ports', views.FrontPortViewSet) -router.register(r'rear-ports', views.RearPortViewSet) -router.register(r'device-bays', views.DeviceBayViewSet) -router.register(r'inventory-items', views.InventoryItemViewSet) +router.register('console-ports', views.ConsolePortViewSet) +router.register('console-server-ports', views.ConsoleServerPortViewSet) +router.register('power-ports', views.PowerPortViewSet) +router.register('power-outlets', views.PowerOutletViewSet) +router.register('interfaces', views.InterfaceViewSet) +router.register('front-ports', views.FrontPortViewSet) +router.register('rear-ports', views.RearPortViewSet) +router.register('device-bays', views.DeviceBayViewSet) +router.register('inventory-items', views.InventoryItemViewSet) # Connections -router.register(r'console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections') -router.register(r'power-connections', views.PowerConnectionViewSet, basename='powerconnections') -router.register(r'interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections') +router.register('console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections') +router.register('power-connections', views.PowerConnectionViewSet, basename='powerconnections') +router.register('interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections') # Cables -router.register(r'cables', views.CableViewSet) +router.register('cables', views.CableViewSet) # Virtual chassis -router.register(r'virtual-chassis', views.VirtualChassisViewSet) +router.register('virtual-chassis', views.VirtualChassisViewSet) # Power -router.register(r'power-panels', views.PowerPanelViewSet) -router.register(r'power-feeds', views.PowerFeedViewSet) +router.register('power-panels', views.PowerPanelViewSet) +router.register('power-feeds', views.PowerFeedViewSet) # Miscellaneous -router.register(r'connected-device', views.ConnectedDeviceViewSet, basename='connected-device') +router.register('connected-device', views.ConnectedDeviceViewSet, basename='connected-device') app_name = 'dcim-api' urlpatterns = router.urls diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index a1ecc95b3..2a000ac24 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -2,7 +2,7 @@ from collections import OrderedDict from django.conf import settings 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 drf_yasg import openapi from drf_yasg.openapi import Parameter @@ -23,7 +23,6 @@ from dcim.models import ( ) from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet -from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph from ipam.models import Prefix, VLAN from utilities.api import ( @@ -41,21 +40,26 @@ from .exceptions import MissingFilterException class DCIMFieldChoicesViewSet(FieldChoicesViewSet): fields = ( - (Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']), - (ConsolePort, ['connection_status']), - (Device, ['face', 'status']), - (DeviceType, ['subdevice_role']), - (FrontPort, ['type']), - (FrontPortTemplate, ['type']), - (Interface, ['type', 'mode']), - (InterfaceTemplate, ['type']), - (PowerOutlet, ['feed_leg']), - (PowerOutletTemplate, ['feed_leg']), - (PowerPort, ['connection_status']), - (Rack, ['outer_unit', 'status', 'type', 'width']), - (RearPort, ['type']), - (RearPortTemplate, ['type']), - (Site, ['status']), + (serializers.CableSerializer, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']), + (serializers.ConsolePortSerializer, ['type', 'connection_status']), + (serializers.ConsolePortTemplateSerializer, ['type']), + (serializers.ConsoleServerPortSerializer, ['type']), + (serializers.ConsoleServerPortTemplateSerializer, ['type']), + (serializers.DeviceSerializer, ['face', 'status']), + (serializers.DeviceTypeSerializer, ['subdevice_role']), + (serializers.FrontPortSerializer, ['type']), + (serializers.FrontPortTemplateSerializer, ['type']), + (serializers.InterfaceSerializer, ['type', 'mode']), + (serializers.InterfaceTemplateSerializer, ['type']), + (serializers.PowerFeedSerializer, ['phase', 'status', 'supply', 'type']), + (serializers.PowerOutletSerializer, ['type', 'feed_leg']), + (serializers.PowerOutletTemplateSerializer, ['type', 'feed_leg']), + (serializers.PowerPortSerializer, ['type', 'connection_status']), + (serializers.PowerPortTemplateSerializer, ['type']), + (serializers.RackSerializer, ['outer_unit', 'status', 'type', 'width']), + (serializers.RearPortSerializer, ['type']), + (serializers.RearPortTemplateSerializer, ['type']), + (serializers.SiteSerializer, ['status']), ) @@ -102,7 +106,7 @@ class RegionViewSet(CustomFieldModelViewSet): site_count=Count('sites') ) 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'), ) serializer_class = serializers.SiteSerializer - filterset_class = filters.SiteFilter + filterset_class = filters.SiteFilterSet @action(detail=True) def graphs(self, request, pk): @@ -129,7 +133,7 @@ class SiteViewSet(CustomFieldModelViewSet): A convenience method for rendering graphs for a particular site. """ site = get_object_or_404(Site, pk=pk) - queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE) + queryset = Graph.objects.filter(type__model='site') serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site}) return Response(serializer.data) @@ -143,7 +147,7 @@ class RackGroupViewSet(ModelViewSet): rack_count=Count('racks') ) serializer_class = serializers.RackGroupSerializer - filterset_class = filters.RackGroupFilter + filterset_class = filters.RackGroupFilterSet # @@ -155,7 +159,7 @@ class RackRoleViewSet(ModelViewSet): rack_count=Count('racks') ) 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') ) serializer_class = serializers.RackSerializer - filterset_class = filters.RackFilter + filterset_class = filters.RackFilterSet + @swagger_auto_schema(deprecated=True) @action(detail=True) def units(self, request, pk=None): """ List rack units (by rack) """ + # TODO: Remove this action detail route in v2.8 rack = get_object_or_404(Rack, pk=pk) - face = request.GET.get('face', 0) + face = request.GET.get('face', 'front') exclude_pk = request.GET.get('exclude', None) if exclude_pk is not None: try: @@ -197,6 +203,50 @@ class RackViewSet(CustomFieldModelViewSet): rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request}) return self.get_paginated_response(rack_units.data) + @swagger_auto_schema( + responses={200: serializers.RackUnitSerializer(many=True)}, + query_serializer=serializers.RackElevationDetailFilterSerializer + ) + @action(detail=True) + def elevation(self, request, pk=None): + """ + Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG. + """ + rack = get_object_or_404(Rack, pk=pk) + serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET) + if not serializer.is_valid(): + return Response(serializer.errors, 400) + data = serializer.validated_data + + if data['render'] == 'svg': + # Render and return the elevation as an SVG drawing with the correct content type + drawing = rack.get_elevation_svg( + 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 @@ -205,7 +255,7 @@ class RackViewSet(CustomFieldModelViewSet): class RackReservationViewSet(ModelViewSet): queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant') serializer_class = serializers.RackReservationSerializer - filterset_class = filters.RackReservationFilter + filterset_class = filters.RackReservationFilterSet # Assign user from request def perform_create(self, serializer): @@ -223,7 +273,7 @@ class ManufacturerViewSet(ModelViewSet): platform_count=get_subquery(Platform, 'manufacturer') ) serializer_class = serializers.ManufacturerSerializer - filterset_class = filters.ManufacturerFilter + filterset_class = filters.ManufacturerFilterSet # @@ -235,7 +285,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet): device_count=Count('instances') ) serializer_class = serializers.DeviceTypeSerializer - filterset_class = filters.DeviceTypeFilter + filterset_class = filters.DeviceTypeFilterSet # @@ -245,49 +295,49 @@ class DeviceTypeViewSet(CustomFieldModelViewSet): class ConsolePortTemplateViewSet(ModelViewSet): queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.ConsolePortTemplateSerializer - filterset_class = filters.ConsolePortTemplateFilter + filterset_class = filters.ConsolePortTemplateFilterSet class ConsoleServerPortTemplateViewSet(ModelViewSet): queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.ConsoleServerPortTemplateSerializer - filterset_class = filters.ConsoleServerPortTemplateFilter + filterset_class = filters.ConsoleServerPortTemplateFilterSet class PowerPortTemplateViewSet(ModelViewSet): queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.PowerPortTemplateSerializer - filterset_class = filters.PowerPortTemplateFilter + filterset_class = filters.PowerPortTemplateFilterSet class PowerOutletTemplateViewSet(ModelViewSet): queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.PowerOutletTemplateSerializer - filterset_class = filters.PowerOutletTemplateFilter + filterset_class = filters.PowerOutletTemplateFilterSet class InterfaceTemplateViewSet(ModelViewSet): queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.InterfaceTemplateSerializer - filterset_class = filters.InterfaceTemplateFilter + filterset_class = filters.InterfaceTemplateFilterSet class FrontPortTemplateViewSet(ModelViewSet): queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.FrontPortTemplateSerializer - filterset_class = filters.FrontPortTemplateFilter + filterset_class = filters.FrontPortTemplateFilterSet class RearPortTemplateViewSet(ModelViewSet): queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.RearPortTemplateSerializer - filterset_class = filters.RearPortTemplateFilter + filterset_class = filters.RearPortTemplateFilterSet class DeviceBayTemplateViewSet(ModelViewSet): queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer') 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') ) 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') ) 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', '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): """ @@ -353,11 +403,22 @@ class DeviceViewSet(CustomFieldModelViewSet): A convenience method for rendering graphs for a particular Device. """ device = get_object_or_404(Device, pk=pk) - queryset = Graph.objects.filter(type=GRAPH_TYPE_DEVICE) + queryset = Graph.objects.filter(type__model='device') serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device}) return Response(serializer.data) + @swagger_auto_schema( + manual_parameters=[ + Parameter( + name='method', + in_='query', + required=True, + type=openapi.TYPE_STRING + ) + ], + responses={'200': serializers.DeviceNAPALMSerializer} + ) @action(detail=True, url_path='napalm') def napalm(self, request, pk): """ @@ -396,13 +457,29 @@ class DeviceViewSet(CustomFieldModelViewSet): napalm_methods = request.GET.getlist('method') response = OrderedDict([(m, None) for m in napalm_methods]) ip_address = str(device.primary_ip.address.ip) + username = settings.NAPALM_USERNAME + password = settings.NAPALM_PASSWORD optional_args = settings.NAPALM_ARGS.copy() if device.platform.napalm_args is not None: optional_args.update(device.platform.napalm_args) + + # Update NAPALM parameters according to the request headers + for header in request.headers: + if header[:9].lower() != 'x-napalm-': + continue + + key = header[9:] + if key.lower() == 'username': + username = request.headers[header] + elif key.lower() == 'password': + password = request.headers[header] + elif key: + optional_args[key.lower()] = request.headers[header] + d = driver( hostname=ip_address, - username=settings.NAPALM_USERNAME, - password=settings.NAPALM_PASSWORD, + username=username, + password=password, timeout=settings.NAPALM_TIMEOUT, optional_args=optional_args ) @@ -437,13 +514,13 @@ class DeviceViewSet(CustomFieldModelViewSet): class ConsolePortViewSet(CableTraceMixin, ModelViewSet): queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') serializer_class = serializers.ConsolePortSerializer - filterset_class = filters.ConsolePortFilter + filterset_class = filters.ConsolePortFilterSet class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet): queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') serializer_class = serializers.ConsoleServerPortSerializer - filterset_class = filters.ConsoleServerPortFilter + filterset_class = filters.ConsoleServerPortFilterSet class PowerPortViewSet(CableTraceMixin, ModelViewSet): @@ -451,13 +528,13 @@ class PowerPortViewSet(CableTraceMixin, ModelViewSet): 'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags' ) serializer_class = serializers.PowerPortSerializer - filterset_class = filters.PowerPortFilter + filterset_class = filters.PowerPortFilterSet class PowerOutletViewSet(CableTraceMixin, ModelViewSet): queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') serializer_class = serializers.PowerOutletSerializer - filterset_class = filters.PowerOutletFilter + filterset_class = filters.PowerOutletFilterSet class InterfaceViewSet(CableTraceMixin, ModelViewSet): @@ -467,7 +544,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet): device__isnull=False ) serializer_class = serializers.InterfaceSerializer - filterset_class = filters.InterfaceFilter + filterset_class = filters.InterfaceFilterSet @action(detail=True) def graphs(self, request, pk): @@ -475,7 +552,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet): A convenience method for rendering graphs for a particular interface. """ interface = get_object_or_404(Interface, pk=pk) - queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE) + queryset = Graph.objects.filter(type__model='interface') serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface}) return Response(serializer.data) @@ -483,25 +560,25 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet): class FrontPortViewSet(ModelViewSet): queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') serializer_class = serializers.FrontPortSerializer - filterset_class = filters.FrontPortFilter + filterset_class = filters.FrontPortFilterSet class RearPortViewSet(ModelViewSet): queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') serializer_class = serializers.RearPortSerializer - filterset_class = filters.RearPortFilter + filterset_class = filters.RearPortFilterSet class DeviceBayViewSet(ModelViewSet): queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags') serializer_class = serializers.DeviceBaySerializer - filterset_class = filters.DeviceBayFilter + filterset_class = filters.DeviceBayFilterSet class InventoryItemViewSet(ModelViewSet): queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') serializer_class = serializers.InventoryItemSerializer - filterset_class = filters.InventoryItemFilter + filterset_class = filters.InventoryItemFilterSet # @@ -515,7 +592,7 @@ class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet): connected_endpoint__isnull=False ) serializer_class = serializers.ConsolePortSerializer - filterset_class = filters.ConsoleConnectionFilter + filterset_class = filters.ConsoleConnectionFilterSet class PowerConnectionViewSet(ListModelMixin, GenericViewSet): @@ -525,7 +602,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet): _connected_poweroutlet__isnull=False ) serializer_class = serializers.PowerPortSerializer - filterset_class = filters.PowerConnectionFilter + filterset_class = filters.PowerConnectionFilterSet class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): @@ -537,7 +614,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): pk__lt=F('_connected_interface') ) serializer_class = serializers.InterfaceConnectionSerializer - filterset_class = filters.InterfaceConnectionFilter + filterset_class = filters.InterfaceConnectionFilterSet # @@ -549,7 +626,7 @@ class CableViewSet(ModelViewSet): 'termination_a', 'termination_b' ) serializer_class = serializers.CableSerializer - filterset_class = filters.CableFilter + filterset_class = filters.CableFilterSet # @@ -561,7 +638,7 @@ class VirtualChassisViewSet(ModelViewSet): member_count=Count('members') ) serializer_class = serializers.VirtualChassisSerializer - filterset_class = filters.VirtualChassisFilter + filterset_class = filters.VirtualChassisFilterSet # @@ -575,7 +652,7 @@ class PowerPanelViewSet(ModelViewSet): powerfeed_count=Count('powerfeeds') ) serializer_class = serializers.PowerPanelSerializer - filterset_class = filters.PowerPanelFilter + filterset_class = filters.PowerPanelFilterSet # @@ -585,7 +662,7 @@ class PowerPanelViewSet(ModelViewSet): class PowerFeedViewSet(CustomFieldModelViewSet): queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags') serializer_class = serializers.PowerFeedSerializer - filterset_class = filters.PowerFeedFilter + filterset_class = filters.PowerFeedFilterSet # diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py new file mode 100644 index 000000000..d4cd066ea --- /dev/null +++ b/netbox/dcim/choices.py @@ -0,0 +1,1092 @@ +from utilities.choices import ChoiceSet + + +# +# Sites +# + +class SiteStatusChoices(ChoiceSet): + + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + STATUS_RETIRED = 'retired' + STATUS_CONSTRUCTION = 'construction' + STATUS_COMMISSIONING = 'commissioning' + + CHOICES = ( + (STATUS_ACTIVE, 'Active'), + (STATUS_PLANNED, 'Planned'), + (STATUS_RETIRED, 'Retired'), + (STATUS_CONSTRUCTION, 'Construction'), + (STATUS_COMMISSIONING, 'Commissioning'), + ) + + LEGACY_MAP = { + STATUS_ACTIVE: 1, + STATUS_PLANNED: 2, + STATUS_RETIRED: 4, + STATUS_CONSTRUCTION: 90, + STATUS_COMMISSIONING: 91, + } + + +# +# Racks +# + +class RackTypeChoices(ChoiceSet): + + TYPE_2POST = '2-post-frame' + TYPE_4POST = '4-post-frame' + TYPE_CABINET = '4-post-cabinet' + TYPE_WALLFRAME = 'wall-frame' + TYPE_WALLCABINET = 'wall-cabinet' + + CHOICES = ( + (TYPE_2POST, '2-post frame'), + (TYPE_4POST, '4-post frame'), + (TYPE_CABINET, '4-post cabinet'), + (TYPE_WALLFRAME, 'Wall-mounted frame'), + (TYPE_WALLCABINET, 'Wall-mounted cabinet'), + ) + + LEGACY_MAP = { + TYPE_2POST: 100, + TYPE_4POST: 200, + TYPE_CABINET: 300, + TYPE_WALLFRAME: 1000, + TYPE_WALLCABINET: 1100, + } + + +class RackWidthChoices(ChoiceSet): + + WIDTH_19IN = 19 + WIDTH_23IN = 23 + + CHOICES = ( + (WIDTH_19IN, '19 inches'), + (WIDTH_23IN, '23 inches'), + ) + + +class RackStatusChoices(ChoiceSet): + + STATUS_RESERVED = 'reserved' + STATUS_AVAILABLE = 'available' + STATUS_PLANNED = 'planned' + STATUS_ACTIVE = 'active' + STATUS_DEPRECATED = 'deprecated' + + CHOICES = ( + (STATUS_RESERVED, 'Reserved'), + (STATUS_AVAILABLE, 'Available'), + (STATUS_PLANNED, 'Planned'), + (STATUS_ACTIVE, 'Active'), + (STATUS_DEPRECATED, 'Deprecated'), + ) + + LEGACY_MAP = { + STATUS_RESERVED: 0, + STATUS_AVAILABLE: 1, + STATUS_PLANNED: 2, + STATUS_ACTIVE: 3, + STATUS_DEPRECATED: 4, + } + + +class RackDimensionUnitChoices(ChoiceSet): + + UNIT_MILLIMETER = 'mm' + UNIT_INCH = 'in' + + CHOICES = ( + (UNIT_MILLIMETER, 'Millimeters'), + (UNIT_INCH, 'Inches'), + ) + + LEGACY_MAP = { + UNIT_MILLIMETER: 1000, + UNIT_INCH: 2000, + } + + +class RackElevationDetailRenderChoices(ChoiceSet): + + RENDER_JSON = 'json' + RENDER_SVG = 'svg' + + CHOICES = ( + (RENDER_JSON, 'json'), + (RENDER_SVG, 'svg') + ) + + +# +# DeviceTypes +# + +class SubdeviceRoleChoices(ChoiceSet): + + ROLE_PARENT = 'parent' + ROLE_CHILD = 'child' + + CHOICES = ( + (ROLE_PARENT, 'Parent'), + (ROLE_CHILD, 'Child'), + ) + + LEGACY_MAP = { + ROLE_PARENT: True, + ROLE_CHILD: False, + } + + +# +# Devices +# + +class DeviceFaceChoices(ChoiceSet): + + FACE_FRONT = 'front' + FACE_REAR = 'rear' + + CHOICES = ( + (FACE_FRONT, 'Front'), + (FACE_REAR, 'Rear'), + ) + + LEGACY_MAP = { + FACE_FRONT: 0, + FACE_REAR: 1, + } + + +class DeviceStatusChoices(ChoiceSet): + + STATUS_OFFLINE = 'offline' + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + STATUS_STAGED = 'staged' + STATUS_FAILED = 'failed' + STATUS_INVENTORY = 'inventory' + STATUS_DECOMMISSIONING = 'decommissioning' + + CHOICES = ( + (STATUS_OFFLINE, 'Offline'), + (STATUS_ACTIVE, 'Active'), + (STATUS_PLANNED, 'Planned'), + (STATUS_STAGED, 'Staged'), + (STATUS_FAILED, 'Failed'), + (STATUS_INVENTORY, 'Inventory'), + (STATUS_DECOMMISSIONING, 'Decommissioning'), + ) + + LEGACY_MAP = { + STATUS_OFFLINE: 0, + STATUS_ACTIVE: 1, + STATUS_PLANNED: 2, + STATUS_STAGED: 3, + STATUS_FAILED: 4, + STATUS_INVENTORY: 5, + STATUS_DECOMMISSIONING: 6, + } + + +# +# ConsolePorts +# + +class ConsolePortTypeChoices(ChoiceSet): + + TYPE_DE9 = 'de-9' + TYPE_DB25 = 'db-25' + TYPE_RJ11 = 'rj-11' + TYPE_RJ12 = 'rj-12' + TYPE_RJ45 = 'rj-45' + TYPE_USB_A = 'usb-a' + TYPE_USB_B = 'usb-b' + TYPE_USB_C = 'usb-c' + TYPE_USB_MINI_A = 'usb-mini-a' + TYPE_USB_MINI_B = 'usb-mini-b' + TYPE_USB_MICRO_A = 'usb-micro-a' + TYPE_USB_MICRO_B = 'usb-micro-b' + TYPE_OTHER = 'other' + + CHOICES = ( + ('Serial', ( + (TYPE_DE9, 'DE-9'), + (TYPE_DB25, 'DB-25'), + (TYPE_RJ11, 'RJ-11'), + (TYPE_RJ12, 'RJ-12'), + (TYPE_RJ45, 'RJ-45'), + )), + ('USB', ( + (TYPE_USB_A, 'USB Type A'), + (TYPE_USB_B, 'USB Type B'), + (TYPE_USB_C, 'USB Type C'), + (TYPE_USB_MINI_A, 'USB Mini A'), + (TYPE_USB_MINI_B, 'USB Mini B'), + (TYPE_USB_MICRO_A, 'USB Micro A'), + (TYPE_USB_MICRO_B, 'USB Micro B'), + )), + ('Other', ( + (TYPE_OTHER, 'Other'), + )), + ) + + +# +# PowerPorts +# + +class PowerPortTypeChoices(ChoiceSet): + + # IEC 60320 + TYPE_IEC_C6 = 'iec-60320-c6' + TYPE_IEC_C8 = 'iec-60320-c8' + TYPE_IEC_C14 = 'iec-60320-c14' + TYPE_IEC_C16 = 'iec-60320-c16' + TYPE_IEC_C20 = 'iec-60320-c20' + # IEC 60309 + TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h' + TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h' + TYPE_IEC_PNE9H = 'iec-60309-p-n-e-9h' + TYPE_IEC_2PE4H = 'iec-60309-2p-e-4h' + TYPE_IEC_2PE6H = 'iec-60309-2p-e-6h' + TYPE_IEC_2PE9H = 'iec-60309-2p-e-9h' + TYPE_IEC_3PE4H = 'iec-60309-3p-e-4h' + TYPE_IEC_3PE6H = 'iec-60309-3p-e-6h' + TYPE_IEC_3PE9H = 'iec-60309-3p-e-9h' + TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' + TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' + TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' + # NEMA non-locking + TYPE_NEMA_515P = 'nema-5-15p' + TYPE_NEMA_520P = 'nema-5-20p' + TYPE_NEMA_530P = 'nema-5-30p' + TYPE_NEMA_550P = 'nema-5-50p' + TYPE_NEMA_615P = 'nema-6-15p' + TYPE_NEMA_620P = 'nema-6-20p' + TYPE_NEMA_630P = 'nema-6-30p' + TYPE_NEMA_650P = 'nema-6-50p' + # NEMA locking + TYPE_NEMA_L515P = 'nema-l5-15p' + TYPE_NEMA_L520P = 'nema-l5-20p' + TYPE_NEMA_L530P = 'nema-l5-30p' + TYPE_NEMA_L615P = 'nema-l5-50p' + TYPE_NEMA_L620P = 'nema-l6-20p' + TYPE_NEMA_L630P = 'nema-l6-30p' + TYPE_NEMA_L650P = 'nema-l6-50p' + # California style + TYPE_CS6361C = 'cs6361c' + TYPE_CS6365C = 'cs6365c' + TYPE_CS8165C = 'cs8165c' + TYPE_CS8265C = 'cs8265c' + TYPE_CS8365C = 'cs8365c' + TYPE_CS8465C = 'cs8465c' + # ITA/international + TYPE_ITA_E = 'ita-e' + TYPE_ITA_F = 'ita-f' + TYPE_ITA_EF = 'ita-ef' + TYPE_ITA_G = 'ita-g' + TYPE_ITA_H = 'ita-h' + TYPE_ITA_I = 'ita-i' + TYPE_ITA_J = 'ita-j' + TYPE_ITA_K = 'ita-k' + TYPE_ITA_L = 'ita-l' + TYPE_ITA_M = 'ita-m' + TYPE_ITA_N = 'ita-n' + TYPE_ITA_O = 'ita-o' + + CHOICES = ( + ('IEC 60320', ( + (TYPE_IEC_C6, 'C6'), + (TYPE_IEC_C8, 'C8'), + (TYPE_IEC_C14, 'C14'), + (TYPE_IEC_C16, 'C16'), + (TYPE_IEC_C20, 'C20'), + )), + ('IEC 60309', ( + (TYPE_IEC_PNE4H, 'P+N+E 4H'), + (TYPE_IEC_PNE6H, 'P+N+E 6H'), + (TYPE_IEC_PNE9H, 'P+N+E 9H'), + (TYPE_IEC_2PE4H, '2P+E 4H'), + (TYPE_IEC_2PE6H, '2P+E 6H'), + (TYPE_IEC_2PE9H, '2P+E 9H'), + (TYPE_IEC_3PE4H, '3P+E 4H'), + (TYPE_IEC_3PE6H, '3P+E 6H'), + (TYPE_IEC_3PE9H, '3P+E 9H'), + (TYPE_IEC_3PNE4H, '3P+N+E 4H'), + (TYPE_IEC_3PNE6H, '3P+N+E 6H'), + (TYPE_IEC_3PNE9H, '3P+N+E 9H'), + )), + ('NEMA (Non-locking)', ( + (TYPE_NEMA_515P, 'NEMA 5-15P'), + (TYPE_NEMA_520P, 'NEMA 5-20P'), + (TYPE_NEMA_530P, 'NEMA 5-30P'), + (TYPE_NEMA_550P, 'NEMA 5-50P'), + (TYPE_NEMA_615P, 'NEMA 6-15P'), + (TYPE_NEMA_620P, 'NEMA 6-20P'), + (TYPE_NEMA_630P, 'NEMA 6-30P'), + (TYPE_NEMA_650P, 'NEMA 6-50P'), + )), + ('NEMA (Locking)', ( + (TYPE_NEMA_L515P, 'NEMA L5-15P'), + (TYPE_NEMA_L520P, 'NEMA L5-20P'), + (TYPE_NEMA_L530P, 'NEMA L5-30P'), + (TYPE_NEMA_L615P, 'NEMA L6-15P'), + (TYPE_NEMA_L620P, 'NEMA L6-20P'), + (TYPE_NEMA_L630P, 'NEMA L6-30P'), + (TYPE_NEMA_L650P, 'NEMA L6-50P'), + )), + ('California Style', ( + (TYPE_CS6361C, 'CS6361C'), + (TYPE_CS6365C, 'CS6365C'), + (TYPE_CS8165C, 'CS8165C'), + (TYPE_CS8265C, 'CS8265C'), + (TYPE_CS8365C, 'CS8365C'), + (TYPE_CS8465C, 'CS8465C'), + )), + ('International/ITA', ( + (TYPE_ITA_E, 'ITA Type E (CEE 7/5)'), + (TYPE_ITA_F, 'ITA Type F (CEE 7/4)'), + (TYPE_ITA_EF, 'ITA Type E/F (CEE 7/7)'), + (TYPE_ITA_G, 'ITA Type G (BS 1363)'), + (TYPE_ITA_H, 'ITA Type H'), + (TYPE_ITA_I, 'ITA Type I'), + (TYPE_ITA_J, 'ITA Type J'), + (TYPE_ITA_K, 'ITA Type K'), + (TYPE_ITA_L, 'ITA Type L (CEI 23-50)'), + (TYPE_ITA_M, 'ITA Type M (BS 546)'), + (TYPE_ITA_N, 'ITA Type N'), + (TYPE_ITA_O, 'ITA Type O'), + )), + ) + + +# +# PowerOutlets +# + +class PowerOutletTypeChoices(ChoiceSet): + + # IEC 60320 + TYPE_IEC_C5 = 'iec-60320-c5' + TYPE_IEC_C7 = 'iec-60320-c7' + TYPE_IEC_C13 = 'iec-60320-c13' + TYPE_IEC_C15 = 'iec-60320-c15' + TYPE_IEC_C19 = 'iec-60320-c19' + # IEC 60309 + TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h' + TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h' + TYPE_IEC_PNE9H = 'iec-60309-p-n-e-9h' + TYPE_IEC_2PE4H = 'iec-60309-2p-e-4h' + TYPE_IEC_2PE6H = 'iec-60309-2p-e-6h' + TYPE_IEC_2PE9H = 'iec-60309-2p-e-9h' + TYPE_IEC_3PE4H = 'iec-60309-3p-e-4h' + TYPE_IEC_3PE6H = 'iec-60309-3p-e-6h' + TYPE_IEC_3PE9H = 'iec-60309-3p-e-9h' + TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' + TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' + TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' + # NEMA non-locking + TYPE_NEMA_515R = 'nema-5-15r' + TYPE_NEMA_520R = 'nema-5-20r' + TYPE_NEMA_530R = 'nema-5-30r' + TYPE_NEMA_550R = 'nema-5-50r' + TYPE_NEMA_615R = 'nema-6-15r' + TYPE_NEMA_620R = 'nema-6-20r' + TYPE_NEMA_630R = 'nema-6-30r' + TYPE_NEMA_650R = 'nema-6-50r' + # NEMA locking + TYPE_NEMA_L515R = 'nema-l5-15r' + TYPE_NEMA_L520R = 'nema-l5-20r' + TYPE_NEMA_L530R = 'nema-l5-30r' + TYPE_NEMA_L615R = 'nema-l5-50r' + TYPE_NEMA_L620R = 'nema-l6-20r' + TYPE_NEMA_L630R = 'nema-l6-30r' + TYPE_NEMA_L650R = 'nema-l6-50r' + # California style + TYPE_CS6360C = 'CS6360C' + TYPE_CS6364C = 'CS6364C' + TYPE_CS8164C = 'CS8164C' + TYPE_CS8264C = 'CS8264C' + TYPE_CS8364C = 'CS8364C' + TYPE_CS8464C = 'CS8464C' + # ITA/international + TYPE_ITA_E = 'ita-e' + TYPE_ITA_F = 'ita-f' + TYPE_ITA_G = 'ita-g' + TYPE_ITA_H = 'ita-h' + TYPE_ITA_I = 'ita-i' + TYPE_ITA_J = 'ita-j' + TYPE_ITA_K = 'ita-k' + TYPE_ITA_L = 'ita-l' + TYPE_ITA_M = 'ita-m' + TYPE_ITA_N = 'ita-n' + TYPE_ITA_O = 'ita-o' + + CHOICES = ( + ('IEC 60320', ( + (TYPE_IEC_C5, 'C5'), + (TYPE_IEC_C7, 'C7'), + (TYPE_IEC_C13, 'C13'), + (TYPE_IEC_C15, 'C15'), + (TYPE_IEC_C19, 'C19'), + )), + ('IEC 60309', ( + (TYPE_IEC_PNE4H, 'P+N+E 4H'), + (TYPE_IEC_PNE6H, 'P+N+E 6H'), + (TYPE_IEC_PNE9H, 'P+N+E 9H'), + (TYPE_IEC_2PE4H, '2P+E 4H'), + (TYPE_IEC_2PE6H, '2P+E 6H'), + (TYPE_IEC_2PE9H, '2P+E 9H'), + (TYPE_IEC_3PE4H, '3P+E 4H'), + (TYPE_IEC_3PE6H, '3P+E 6H'), + (TYPE_IEC_3PE9H, '3P+E 9H'), + (TYPE_IEC_3PNE4H, '3P+N+E 4H'), + (TYPE_IEC_3PNE6H, '3P+N+E 6H'), + (TYPE_IEC_3PNE9H, '3P+N+E 9H'), + )), + ('NEMA (Non-locking)', ( + (TYPE_NEMA_515R, 'NEMA 5-15R'), + (TYPE_NEMA_520R, 'NEMA 5-20R'), + (TYPE_NEMA_530R, 'NEMA 5-30R'), + (TYPE_NEMA_550R, 'NEMA 5-50R'), + (TYPE_NEMA_615R, 'NEMA 6-15R'), + (TYPE_NEMA_620R, 'NEMA 6-20R'), + (TYPE_NEMA_630R, 'NEMA 6-30R'), + (TYPE_NEMA_650R, 'NEMA 6-50R'), + )), + ('NEMA (Locking)', ( + (TYPE_NEMA_L515R, 'NEMA L5-15R'), + (TYPE_NEMA_L520R, 'NEMA L5-20R'), + (TYPE_NEMA_L530R, 'NEMA L5-30R'), + (TYPE_NEMA_L615R, 'NEMA L6-15R'), + (TYPE_NEMA_L620R, 'NEMA L6-20R'), + (TYPE_NEMA_L630R, 'NEMA L6-30R'), + (TYPE_NEMA_L650R, 'NEMA L6-50R'), + )), + ('California Style', ( + (TYPE_CS6360C, 'CS6360C'), + (TYPE_CS6364C, 'CS6364C'), + (TYPE_CS8164C, 'CS8164C'), + (TYPE_CS8264C, 'CS8264C'), + (TYPE_CS8364C, 'CS8364C'), + (TYPE_CS8464C, 'CS8464C'), + )), + ('ITA/International', ( + (TYPE_ITA_E, 'ITA Type E (CEE7/5)'), + (TYPE_ITA_F, 'ITA Type F (CEE7/3)'), + (TYPE_ITA_G, 'ITA Type G (BS 1363)'), + (TYPE_ITA_H, 'ITA Type H'), + (TYPE_ITA_I, 'ITA Type I'), + (TYPE_ITA_J, 'ITA Type J'), + (TYPE_ITA_K, 'ITA Type K'), + (TYPE_ITA_L, 'ITA Type L (CEI 23-50)'), + (TYPE_ITA_M, 'ITA Type M (BS 546)'), + (TYPE_ITA_N, 'ITA Type N'), + (TYPE_ITA_O, 'ITA Type O'), + )), + ) + + +class PowerOutletFeedLegChoices(ChoiceSet): + + FEED_LEG_A = 'A' + FEED_LEG_B = 'B' + FEED_LEG_C = 'C' + + CHOICES = ( + (FEED_LEG_A, 'A'), + (FEED_LEG_B, 'B'), + (FEED_LEG_C, 'C'), + ) + + LEGACY_MAP = { + FEED_LEG_A: 1, + FEED_LEG_B: 2, + FEED_LEG_C: 3, + } + + +# +# Interfaces +# + +class InterfaceTypeChoices(ChoiceSet): + + # Virtual + TYPE_VIRTUAL = 'virtual' + TYPE_LAG = 'lag' + + # Ethernet + TYPE_100ME_FIXED = '100base-tx' + TYPE_1GE_FIXED = '1000base-t' + TYPE_1GE_GBIC = '1000base-x-gbic' + TYPE_1GE_SFP = '1000base-x-sfp' + TYPE_2GE_FIXED = '2.5gbase-t' + TYPE_5GE_FIXED = '5gbase-t' + TYPE_10GE_FIXED = '10gbase-t' + TYPE_10GE_CX4 = '10gbase-cx4' + TYPE_10GE_SFP_PLUS = '10gbase-x-sfpp' + TYPE_10GE_XFP = '10gbase-x-xfp' + TYPE_10GE_XENPAK = '10gbase-x-xenpak' + TYPE_10GE_X2 = '10gbase-x-x2' + TYPE_25GE_SFP28 = '25gbase-x-sfp28' + TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp' + TYPE_50GE_QSFP28 = '50gbase-x-sfp28' + TYPE_100GE_CFP = '100gbase-x-cfp' + TYPE_100GE_CFP2 = '100gbase-x-cfp2' + TYPE_100GE_CFP4 = '100gbase-x-cfp4' + TYPE_100GE_CPAK = '100gbase-x-cpak' + TYPE_100GE_QSFP28 = '100gbase-x-qsfp28' + TYPE_200GE_CFP2 = '200gbase-x-cfp2' + TYPE_200GE_QSFP56 = '200gbase-x-qsfp56' + TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd' + TYPE_400GE_OSFP = '400gbase-x-osfp' + + # Wireless + TYPE_80211A = 'ieee802.11a' + TYPE_80211G = 'ieee802.11g' + TYPE_80211N = 'ieee802.11n' + TYPE_80211AC = 'ieee802.11ac' + TYPE_80211AD = 'ieee802.11ad' + TYPE_80211AX = 'ieee802.11ax' + + # Cellular + TYPE_GSM = 'gsm' + TYPE_CDMA = 'cdma' + TYPE_LTE = 'lte' + + # SONET + TYPE_SONET_OC3 = 'sonet-oc3' + TYPE_SONET_OC12 = 'sonet-oc12' + TYPE_SONET_OC48 = 'sonet-oc48' + TYPE_SONET_OC192 = 'sonet-oc192' + TYPE_SONET_OC768 = 'sonet-oc768' + TYPE_SONET_OC1920 = 'sonet-oc1920' + TYPE_SONET_OC3840 = 'sonet-oc3840' + + # Fibrechannel + TYPE_1GFC_SFP = '1gfc-sfp' + TYPE_2GFC_SFP = '2gfc-sfp' + TYPE_4GFC_SFP = '4gfc-sfp' + TYPE_8GFC_SFP_PLUS = '8gfc-sfpp' + TYPE_16GFC_SFP_PLUS = '16gfc-sfpp' + TYPE_32GFC_SFP28 = '32gfc-sfp28' + TYPE_128GFC_QSFP28 = '128gfc-sfp28' + + # InfiniBand + TYPE_INFINIBAND_SDR = 'inifiband-sdr' + TYPE_INFINIBAND_DDR = 'inifiband-ddr' + TYPE_INFINIBAND_QDR = 'inifiband-qdr' + TYPE_INFINIBAND_FDR10 = 'inifiband-fdr10' + TYPE_INFINIBAND_FDR = 'inifiband-fdr' + TYPE_INFINIBAND_EDR = 'inifiband-edr' + TYPE_INFINIBAND_HDR = 'inifiband-hdr' + TYPE_INFINIBAND_NDR = 'inifiband-ndr' + TYPE_INFINIBAND_XDR = 'inifiband-xdr' + + # Serial + TYPE_T1 = 't1' + TYPE_E1 = 'e1' + TYPE_T3 = 't3' + TYPE_E3 = 'e3' + + # Stacking + TYPE_STACKWISE = 'cisco-stackwise' + TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus' + TYPE_FLEXSTACK = 'cisco-flexstack' + TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus' + TYPE_JUNIPER_VCP = 'juniper-vcp' + TYPE_SUMMITSTACK = 'extreme-summitstack' + TYPE_SUMMITSTACK128 = 'extreme-summitstack-128' + TYPE_SUMMITSTACK256 = 'extreme-summitstack-256' + TYPE_SUMMITSTACK512 = 'extreme-summitstack-512' + + # Other + TYPE_OTHER = 'other' + TYPE_KEYSTONE = 'keystone' + + CHOICES = ( + ( + 'Virtual interfaces', + ( + (TYPE_VIRTUAL, 'Virtual'), + (TYPE_LAG, 'Link Aggregation Group (LAG)'), + ), + ), + ( + 'Ethernet (fixed)', + ( + (TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'), + (TYPE_1GE_FIXED, '1000BASE-T (1GE)'), + (TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'), + (TYPE_5GE_FIXED, '5GBASE-T (5GE)'), + (TYPE_10GE_FIXED, '10GBASE-T (10GE)'), + (TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'), + ) + ), + ( + 'Ethernet (modular)', + ( + (TYPE_1GE_GBIC, 'GBIC (1GE)'), + (TYPE_1GE_SFP, 'SFP (1GE)'), + (TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'), + (TYPE_10GE_XFP, 'XFP (10GE)'), + (TYPE_10GE_XENPAK, 'XENPAK (10GE)'), + (TYPE_10GE_X2, 'X2 (10GE)'), + (TYPE_25GE_SFP28, 'SFP28 (25GE)'), + (TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'), + (TYPE_50GE_QSFP28, 'QSFP28 (50GE)'), + (TYPE_100GE_CFP, 'CFP (100GE)'), + (TYPE_100GE_CFP2, 'CFP2 (100GE)'), + (TYPE_200GE_CFP2, 'CFP2 (200GE)'), + (TYPE_100GE_CFP4, 'CFP4 (100GE)'), + (TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'), + (TYPE_100GE_QSFP28, 'QSFP28 (100GE)'), + (TYPE_200GE_QSFP56, 'QSFP56 (200GE)'), + (TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'), + (TYPE_400GE_OSFP, 'OSFP (400GE)'), + ) + ), + ( + 'Wireless', + ( + (TYPE_80211A, 'IEEE 802.11a'), + (TYPE_80211G, 'IEEE 802.11b/g'), + (TYPE_80211N, 'IEEE 802.11n'), + (TYPE_80211AC, 'IEEE 802.11ac'), + (TYPE_80211AD, 'IEEE 802.11ad'), + (TYPE_80211AX, 'IEEE 802.11ax'), + ) + ), + ( + 'Cellular', + ( + (TYPE_GSM, 'GSM'), + (TYPE_CDMA, 'CDMA'), + (TYPE_LTE, 'LTE'), + ) + ), + ( + 'SONET', + ( + (TYPE_SONET_OC3, 'OC-3/STM-1'), + (TYPE_SONET_OC12, 'OC-12/STM-4'), + (TYPE_SONET_OC48, 'OC-48/STM-16'), + (TYPE_SONET_OC192, 'OC-192/STM-64'), + (TYPE_SONET_OC768, 'OC-768/STM-256'), + (TYPE_SONET_OC1920, 'OC-1920/STM-640'), + (TYPE_SONET_OC3840, 'OC-3840/STM-1234'), + ) + ), + ( + 'FibreChannel', + ( + (TYPE_1GFC_SFP, 'SFP (1GFC)'), + (TYPE_2GFC_SFP, 'SFP (2GFC)'), + (TYPE_4GFC_SFP, 'SFP (4GFC)'), + (TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'), + (TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'), + (TYPE_32GFC_SFP28, 'SFP28 (32GFC)'), + (TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'), + ) + ), + ( + 'InfiniBand', + ( + (TYPE_INFINIBAND_SDR, 'SDR (2 Gbps)'), + (TYPE_INFINIBAND_DDR, 'DDR (4 Gbps)'), + (TYPE_INFINIBAND_QDR, 'QDR (8 Gbps)'), + (TYPE_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'), + (TYPE_INFINIBAND_FDR, 'FDR (13.5 Gbps)'), + (TYPE_INFINIBAND_EDR, 'EDR (25 Gbps)'), + (TYPE_INFINIBAND_HDR, 'HDR (50 Gbps)'), + (TYPE_INFINIBAND_NDR, 'NDR (100 Gbps)'), + (TYPE_INFINIBAND_XDR, 'XDR (250 Gbps)'), + ) + ), + ( + 'Serial', + ( + (TYPE_T1, 'T1 (1.544 Mbps)'), + (TYPE_E1, 'E1 (2.048 Mbps)'), + (TYPE_T3, 'T3 (45 Mbps)'), + (TYPE_E3, 'E3 (34 Mbps)'), + ) + ), + ( + 'Stacking', + ( + (TYPE_STACKWISE, 'Cisco StackWise'), + (TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'), + (TYPE_FLEXSTACK, 'Cisco FlexStack'), + (TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'), + (TYPE_JUNIPER_VCP, 'Juniper VCP'), + (TYPE_SUMMITSTACK, 'Extreme SummitStack'), + (TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'), + (TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'), + (TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'), + ) + ), + ( + 'Other', + ( + (TYPE_KEYSTONE, 'Keystone'), + (TYPE_OTHER, 'Other'), + ) + ), + ) + + LEGACY_MAP = { + TYPE_VIRTUAL: 0, + TYPE_LAG: 200, + TYPE_100ME_FIXED: 800, + TYPE_1GE_FIXED: 1000, + TYPE_1GE_GBIC: 1050, + TYPE_1GE_SFP: 1100, + TYPE_2GE_FIXED: 1120, + TYPE_5GE_FIXED: 1130, + TYPE_10GE_FIXED: 1150, + TYPE_10GE_CX4: 1170, + TYPE_10GE_SFP_PLUS: 1200, + TYPE_10GE_XFP: 1300, + TYPE_10GE_XENPAK: 1310, + TYPE_10GE_X2: 1320, + TYPE_25GE_SFP28: 1350, + TYPE_40GE_QSFP_PLUS: 1400, + TYPE_50GE_QSFP28: 1420, + TYPE_100GE_CFP: 1500, + TYPE_100GE_CFP2: 1510, + TYPE_100GE_CFP4: 1520, + TYPE_100GE_CPAK: 1550, + TYPE_100GE_QSFP28: 1600, + TYPE_200GE_CFP2: 1650, + TYPE_200GE_QSFP56: 1700, + TYPE_400GE_QSFP_DD: 1750, + TYPE_400GE_OSFP: 1800, + TYPE_80211A: 2600, + TYPE_80211G: 2610, + TYPE_80211N: 2620, + TYPE_80211AC: 2630, + TYPE_80211AD: 2640, + TYPE_GSM: 2810, + TYPE_CDMA: 2820, + TYPE_LTE: 2830, + TYPE_SONET_OC3: 6100, + TYPE_SONET_OC12: 6200, + TYPE_SONET_OC48: 6300, + TYPE_SONET_OC192: 6400, + TYPE_SONET_OC768: 6500, + TYPE_SONET_OC1920: 6600, + TYPE_SONET_OC3840: 6700, + TYPE_1GFC_SFP: 3010, + TYPE_2GFC_SFP: 3020, + TYPE_4GFC_SFP: 3040, + TYPE_8GFC_SFP_PLUS: 3080, + TYPE_16GFC_SFP_PLUS: 3160, + TYPE_32GFC_SFP28: 3320, + TYPE_128GFC_QSFP28: 3400, + TYPE_INFINIBAND_SDR: 7010, + TYPE_INFINIBAND_DDR: 7020, + TYPE_INFINIBAND_QDR: 7030, + TYPE_INFINIBAND_FDR10: 7040, + TYPE_INFINIBAND_FDR: 7050, + TYPE_INFINIBAND_EDR: 7060, + TYPE_INFINIBAND_HDR: 7070, + TYPE_INFINIBAND_NDR: 7080, + TYPE_INFINIBAND_XDR: 7090, + TYPE_T1: 4000, + TYPE_E1: 4010, + TYPE_T3: 4040, + TYPE_E3: 4050, + TYPE_STACKWISE: 5000, + TYPE_STACKWISE_PLUS: 5050, + TYPE_FLEXSTACK: 5100, + TYPE_FLEXSTACK_PLUS: 5150, + TYPE_JUNIPER_VCP: 5200, + TYPE_SUMMITSTACK: 5300, + TYPE_SUMMITSTACK128: 5310, + TYPE_SUMMITSTACK256: 5320, + TYPE_SUMMITSTACK512: 5330, + TYPE_OTHER: 32767, + TYPE_KEYSTONE: 32766, + } + + +class InterfaceModeChoices(ChoiceSet): + + MODE_ACCESS = 'access' + MODE_TAGGED = 'tagged' + MODE_TAGGED_ALL = 'tagged-all' + + CHOICES = ( + (MODE_ACCESS, 'Access'), + (MODE_TAGGED, 'Tagged'), + (MODE_TAGGED_ALL, 'Tagged (All)'), + ) + + LEGACY_MAP = { + MODE_ACCESS: 100, + MODE_TAGGED: 200, + MODE_TAGGED_ALL: 300, + } + + +# +# FrontPorts/RearPorts +# + +class PortTypeChoices(ChoiceSet): + + TYPE_8P8C = '8p8c' + TYPE_110_PUNCH = '110-punch' + TYPE_BNC = 'bnc' + TYPE_ST = 'st' + TYPE_SC = 'sc' + TYPE_SC_APC = 'sc-apc' + TYPE_FC = 'fc' + TYPE_LC = 'lc' + TYPE_LC_APC = 'lc-apc' + TYPE_MTRJ = 'mtrj' + TYPE_MPO = 'mpo' + TYPE_LSH = 'lsh' + TYPE_LSH_APC = 'lsh-apc' + + CHOICES = ( + ( + 'Copper', + ( + (TYPE_8P8C, '8P8C'), + (TYPE_110_PUNCH, '110 Punch'), + (TYPE_BNC, 'BNC'), + ), + ), + ( + 'Fiber Optic', + ( + (TYPE_FC, 'FC'), + (TYPE_LC, 'LC'), + (TYPE_LC_APC, 'LC/APC'), + (TYPE_LSH, 'LSH'), + (TYPE_LSH_APC, 'LSH/APC'), + (TYPE_MPO, 'MPO'), + (TYPE_MTRJ, 'MTRJ'), + (TYPE_SC, 'SC'), + (TYPE_SC_APC, 'SC/APC'), + (TYPE_ST, 'ST'), + ) + ) + ) + + LEGACY_MAP = { + TYPE_8P8C: 1000, + TYPE_110_PUNCH: 1100, + TYPE_BNC: 1200, + TYPE_ST: 2000, + TYPE_SC: 2100, + TYPE_SC_APC: 2110, + TYPE_FC: 2200, + TYPE_LC: 2300, + TYPE_LC_APC: 2310, + TYPE_MTRJ: 2400, + TYPE_MPO: 2500, + TYPE_LSH: 2600, + TYPE_LSH_APC: 2610, + } + + +# +# Cables +# + +class CableTypeChoices(ChoiceSet): + + TYPE_CAT3 = 'cat3' + TYPE_CAT5 = 'cat5' + TYPE_CAT5E = 'cat5e' + TYPE_CAT6 = 'cat6' + TYPE_CAT6A = 'cat6a' + TYPE_CAT7 = 'cat7' + TYPE_DAC_ACTIVE = 'dac-active' + TYPE_DAC_PASSIVE = 'dac-passive' + TYPE_COAXIAL = 'coaxial' + TYPE_MMF = 'mmf' + TYPE_MMF_OM1 = 'mmf-om1' + TYPE_MMF_OM2 = 'mmf-om2' + TYPE_MMF_OM3 = 'mmf-om3' + TYPE_MMF_OM4 = 'mmf-om4' + TYPE_SMF = 'smf' + TYPE_SMF_OS1 = 'smf-os1' + TYPE_SMF_OS2 = 'smf-os2' + TYPE_AOC = 'aoc' + TYPE_POWER = 'power' + + CHOICES = ( + ( + 'Copper', ( + (TYPE_CAT3, 'CAT3'), + (TYPE_CAT5, 'CAT5'), + (TYPE_CAT5E, 'CAT5e'), + (TYPE_CAT6, 'CAT6'), + (TYPE_CAT6A, 'CAT6a'), + (TYPE_CAT7, 'CAT7'), + (TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'), + (TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'), + (TYPE_COAXIAL, 'Coaxial'), + ), + ), + ( + 'Fiber', ( + (TYPE_MMF, 'Multimode Fiber'), + (TYPE_MMF_OM1, 'Multimode Fiber (OM1)'), + (TYPE_MMF_OM2, 'Multimode Fiber (OM2)'), + (TYPE_MMF_OM3, 'Multimode Fiber (OM3)'), + (TYPE_MMF_OM4, 'Multimode Fiber (OM4)'), + (TYPE_SMF, 'Singlemode Fiber'), + (TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'), + (TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'), + (TYPE_AOC, 'Active Optical Cabling (AOC)'), + ), + ), + (TYPE_POWER, 'Power'), + ) + + LEGACY_MAP = { + TYPE_CAT3: 1300, + TYPE_CAT5: 1500, + TYPE_CAT5E: 1510, + TYPE_CAT6: 1600, + TYPE_CAT6A: 1610, + TYPE_CAT7: 1700, + TYPE_DAC_ACTIVE: 1800, + TYPE_DAC_PASSIVE: 1810, + TYPE_COAXIAL: 1900, + TYPE_MMF: 3000, + TYPE_MMF_OM1: 3010, + TYPE_MMF_OM2: 3020, + TYPE_MMF_OM3: 3030, + TYPE_MMF_OM4: 3040, + TYPE_SMF: 3500, + TYPE_SMF_OS1: 3510, + TYPE_SMF_OS2: 3520, + TYPE_AOC: 3800, + TYPE_POWER: 5000, + } + + +class CableStatusChoices(ChoiceSet): + + STATUS_CONNECTED = 'connected' + STATUS_PLANNED = 'planned' + STATUS_DECOMMISSIONING = 'decommissioning' + + CHOICES = ( + (STATUS_CONNECTED, 'Connected'), + (STATUS_PLANNED, 'Planned'), + (STATUS_DECOMMISSIONING, 'Decommissioning'), + ) + + LEGACY_MAP = { + STATUS_CONNECTED: True, + STATUS_PLANNED: False, + } + + +class CableLengthUnitChoices(ChoiceSet): + + UNIT_METER = 'm' + UNIT_CENTIMETER = 'cm' + UNIT_FOOT = 'ft' + UNIT_INCH = 'in' + + CHOICES = ( + (UNIT_METER, 'Meters'), + (UNIT_CENTIMETER, 'Centimeters'), + (UNIT_FOOT, 'Feet'), + (UNIT_INCH, 'Inches'), + ) + + LEGACY_MAP = { + UNIT_METER: 1200, + UNIT_CENTIMETER: 1100, + UNIT_FOOT: 2100, + UNIT_INCH: 2000, + } + + +# +# PowerFeeds +# + +class PowerFeedStatusChoices(ChoiceSet): + + STATUS_OFFLINE = 'offline' + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + STATUS_FAILED = 'failed' + + CHOICES = ( + (STATUS_OFFLINE, 'Offline'), + (STATUS_ACTIVE, 'Active'), + (STATUS_PLANNED, 'Planned'), + (STATUS_FAILED, 'Failed'), + ) + + LEGACY_MAP = { + STATUS_OFFLINE: 0, + STATUS_ACTIVE: 1, + STATUS_PLANNED: 2, + STATUS_FAILED: 4, + } + + +class PowerFeedTypeChoices(ChoiceSet): + + TYPE_PRIMARY = 'primary' + TYPE_REDUNDANT = 'redundant' + + CHOICES = ( + (TYPE_PRIMARY, 'Primary'), + (TYPE_REDUNDANT, 'Redundant'), + ) + + LEGACY_MAP = { + TYPE_PRIMARY: 1, + TYPE_REDUNDANT: 2, + } + + +class PowerFeedSupplyChoices(ChoiceSet): + + SUPPLY_AC = 'ac' + SUPPLY_DC = 'dc' + + CHOICES = ( + (SUPPLY_AC, 'AC'), + (SUPPLY_DC, 'DC'), + ) + + LEGACY_MAP = { + SUPPLY_AC: 1, + SUPPLY_DC: 2, + } + + +class PowerFeedPhaseChoices(ChoiceSet): + + PHASE_SINGLE = 'single-phase' + PHASE_3PHASE = 'three-phase' + + CHOICES = ( + (PHASE_SINGLE, 'Single phase'), + (PHASE_3PHASE, 'Three-phase'), + ) + + LEGACY_MAP = { + PHASE_SINGLE: 1, + PHASE_3PHASE: 3, + } diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index a067bcf66..78a418283 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -1,461 +1,89 @@ +from django.db.models import Q -# Rack types -RACK_TYPE_2POST = 100 -RACK_TYPE_4POST = 200 -RACK_TYPE_CABINET = 300 -RACK_TYPE_WALLFRAME = 1000 -RACK_TYPE_WALLCABINET = 1100 -RACK_TYPE_CHOICES = ( - (RACK_TYPE_2POST, '2-post frame'), - (RACK_TYPE_4POST, '4-post frame'), - (RACK_TYPE_CABINET, '4-post cabinet'), - (RACK_TYPE_WALLFRAME, 'Wall-mounted frame'), - (RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'), -) - -# Rack widths -RACK_WIDTH_19IN = 19 -RACK_WIDTH_23IN = 23 -RACK_WIDTH_CHOICES = ( - (RACK_WIDTH_19IN, '19 inches'), - (RACK_WIDTH_23IN, '23 inches'), -) - -# Rack faces -RACK_FACE_FRONT = 0 -RACK_FACE_REAR = 1 -RACK_FACE_CHOICES = [ - [RACK_FACE_FRONT, 'Front'], - [RACK_FACE_REAR, 'Rear'], -] - -# Rack statuses -RACK_STATUS_RESERVED = 0 -RACK_STATUS_AVAILABLE = 1 -RACK_STATUS_PLANNED = 2 -RACK_STATUS_ACTIVE = 3 -RACK_STATUS_DEPRECATED = 4 -RACK_STATUS_CHOICES = [ - [RACK_STATUS_ACTIVE, 'Active'], - [RACK_STATUS_PLANNED, 'Planned'], - [RACK_STATUS_RESERVED, 'Reserved'], - [RACK_STATUS_AVAILABLE, 'Available'], - [RACK_STATUS_DEPRECATED, 'Deprecated'], -] - -# Device rack position -DEVICE_POSITION_CHOICES = [ - # Rack.u_height is limited to 100 - (i, 'Unit {}'.format(i)) for i in range(1, 101) -] - -# Parent/child device roles -SUBDEVICE_ROLE_PARENT = True -SUBDEVICE_ROLE_CHILD = False -SUBDEVICE_ROLE_CHOICES = ( - (None, 'None'), - (SUBDEVICE_ROLE_PARENT, 'Parent'), - (SUBDEVICE_ROLE_CHILD, 'Child'), -) - -# Interface types -# Virtual -IFACE_TYPE_VIRTUAL = 0 -IFACE_TYPE_LAG = 200 -# Ethernet -IFACE_TYPE_100ME_FIXED = 800 -IFACE_TYPE_1GE_FIXED = 1000 -IFACE_TYPE_1GE_GBIC = 1050 -IFACE_TYPE_1GE_SFP = 1100 -IFACE_TYPE_2GE_FIXED = 1120 -IFACE_TYPE_5GE_FIXED = 1130 -IFACE_TYPE_10GE_FIXED = 1150 -IFACE_TYPE_10GE_CX4 = 1170 -IFACE_TYPE_10GE_SFP_PLUS = 1200 -IFACE_TYPE_10GE_XFP = 1300 -IFACE_TYPE_10GE_XENPAK = 1310 -IFACE_TYPE_10GE_X2 = 1320 -IFACE_TYPE_25GE_SFP28 = 1350 -IFACE_TYPE_40GE_QSFP_PLUS = 1400 -IFACE_TYPE_50GE_QSFP28 = 1420 -IFACE_TYPE_100GE_CFP = 1500 -IFACE_TYPE_100GE_CFP2 = 1510 -IFACE_TYPE_100GE_CFP4 = 1520 -IFACE_TYPE_100GE_CPAK = 1550 -IFACE_TYPE_100GE_QSFP28 = 1600 -IFACE_TYPE_200GE_CFP2 = 1650 -IFACE_TYPE_200GE_QSFP56 = 1700 -IFACE_TYPE_400GE_QSFP_DD = 1750 -IFACE_TYPE_400GE_OSFP = 1800 -# Wireless -IFACE_TYPE_80211A = 2600 -IFACE_TYPE_80211G = 2610 -IFACE_TYPE_80211N = 2620 -IFACE_TYPE_80211AC = 2630 -IFACE_TYPE_80211AD = 2640 -# Cellular -IFACE_TYPE_GSM = 2810 -IFACE_TYPE_CDMA = 2820 -IFACE_TYPE_LTE = 2830 -# SONET -IFACE_TYPE_SONET_OC3 = 6100 -IFACE_TYPE_SONET_OC12 = 6200 -IFACE_TYPE_SONET_OC48 = 6300 -IFACE_TYPE_SONET_OC192 = 6400 -IFACE_TYPE_SONET_OC768 = 6500 -IFACE_TYPE_SONET_OC1920 = 6600 -IFACE_TYPE_SONET_OC3840 = 6700 -# Fibrechannel -IFACE_TYPE_1GFC_SFP = 3010 -IFACE_TYPE_2GFC_SFP = 3020 -IFACE_TYPE_4GFC_SFP = 3040 -IFACE_TYPE_8GFC_SFP_PLUS = 3080 -IFACE_TYPE_16GFC_SFP_PLUS = 3160 -IFACE_TYPE_32GFC_SFP28 = 3320 -IFACE_TYPE_128GFC_QSFP28 = 3400 -# InfiniBand -IFACE_FF_INFINIBAND_SDR = 7010 -IFACE_FF_INFINIBAND_DDR = 7020 -IFACE_FF_INFINIBAND_QDR = 7030 -IFACE_FF_INFINIBAND_FDR10 = 7040 -IFACE_FF_INFINIBAND_FDR = 7050 -IFACE_FF_INFINIBAND_EDR = 7060 -IFACE_FF_INFINIBAND_HDR = 7070 -IFACE_FF_INFINIBAND_NDR = 7080 -IFACE_FF_INFINIBAND_XDR = 7090 -# Serial -IFACE_TYPE_T1 = 4000 -IFACE_TYPE_E1 = 4010 -IFACE_TYPE_T3 = 4040 -IFACE_TYPE_E3 = 4050 -# Stacking -IFACE_TYPE_STACKWISE = 5000 -IFACE_TYPE_STACKWISE_PLUS = 5050 -IFACE_TYPE_FLEXSTACK = 5100 -IFACE_TYPE_FLEXSTACK_PLUS = 5150 -IFACE_TYPE_JUNIPER_VCP = 5200 -IFACE_TYPE_SUMMITSTACK = 5300 -IFACE_TYPE_SUMMITSTACK128 = 5310 -IFACE_TYPE_SUMMITSTACK256 = 5320 -IFACE_TYPE_SUMMITSTACK512 = 5330 - -# Other -IFACE_TYPE_OTHER = 32767 -IFACE_TYPE_KEYSTONE = 32766 +from .choices import InterfaceTypeChoices -IFACE_TYPE_CHOICES = [ - [ - 'Virtual interfaces', - [ - [IFACE_TYPE_VIRTUAL, 'Virtual'], - [IFACE_TYPE_LAG, 'Link Aggregation Group (LAG)'], - ], - ], - [ - 'Ethernet (fixed)', - [ - [IFACE_TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'], - [IFACE_TYPE_1GE_FIXED, '1000BASE-T (1GE)'], - [IFACE_TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'], - [IFACE_TYPE_5GE_FIXED, '5GBASE-T (5GE)'], - [IFACE_TYPE_10GE_FIXED, '10GBASE-T (10GE)'], - [IFACE_TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'], - ] - ], - [ - 'Ethernet (modular)', - [ - [IFACE_TYPE_1GE_GBIC, 'GBIC (1GE)'], - [IFACE_TYPE_1GE_SFP, 'SFP (1GE)'], - [IFACE_TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'], - [IFACE_TYPE_10GE_XFP, 'XFP (10GE)'], - [IFACE_TYPE_10GE_XENPAK, 'XENPAK (10GE)'], - [IFACE_TYPE_10GE_X2, 'X2 (10GE)'], - [IFACE_TYPE_25GE_SFP28, 'SFP28 (25GE)'], - [IFACE_TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'], - [IFACE_TYPE_50GE_QSFP28, 'QSFP28 (50GE)'], - [IFACE_TYPE_100GE_CFP, 'CFP (100GE)'], - [IFACE_TYPE_100GE_CFP2, 'CFP2 (100GE)'], - [IFACE_TYPE_200GE_CFP2, 'CFP2 (200GE)'], - [IFACE_TYPE_100GE_CFP4, 'CFP4 (100GE)'], - [IFACE_TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'], - [IFACE_TYPE_100GE_QSFP28, 'QSFP28 (100GE)'], - [IFACE_TYPE_200GE_QSFP56, 'QSFP56 (200GE)'], - [IFACE_TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'], - [IFACE_TYPE_400GE_OSFP, 'OSFP (400GE)'], - ] - ], - [ - 'Wireless', - [ - [IFACE_TYPE_80211A, 'IEEE 802.11a'], - [IFACE_TYPE_80211G, 'IEEE 802.11b/g'], - [IFACE_TYPE_80211N, 'IEEE 802.11n'], - [IFACE_TYPE_80211AC, 'IEEE 802.11ac'], - [IFACE_TYPE_80211AD, 'IEEE 802.11ad'], - ] - ], - [ - 'Cellular', - [ - [IFACE_TYPE_GSM, 'GSM'], - [IFACE_TYPE_CDMA, 'CDMA'], - [IFACE_TYPE_LTE, 'LTE'], - ] - ], - [ - 'SONET', - [ - [IFACE_TYPE_SONET_OC3, 'OC-3/STM-1'], - [IFACE_TYPE_SONET_OC12, 'OC-12/STM-4'], - [IFACE_TYPE_SONET_OC48, 'OC-48/STM-16'], - [IFACE_TYPE_SONET_OC192, 'OC-192/STM-64'], - [IFACE_TYPE_SONET_OC768, 'OC-768/STM-256'], - [IFACE_TYPE_SONET_OC1920, 'OC-1920/STM-640'], - [IFACE_TYPE_SONET_OC3840, 'OC-3840/STM-1234'], - ] - ], - [ - 'FibreChannel', - [ - [IFACE_TYPE_1GFC_SFP, 'SFP (1GFC)'], - [IFACE_TYPE_2GFC_SFP, 'SFP (2GFC)'], - [IFACE_TYPE_4GFC_SFP, 'SFP (4GFC)'], - [IFACE_TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'], - [IFACE_TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'], - [IFACE_TYPE_32GFC_SFP28, 'SFP28 (32GFC)'], - [IFACE_TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'], - ] - ], - [ - 'InfiniBand', - [ - [IFACE_FF_INFINIBAND_SDR, 'SDR (2 Gbps)'], - [IFACE_FF_INFINIBAND_DDR, 'DDR (4 Gbps)'], - [IFACE_FF_INFINIBAND_QDR, 'QDR (8 Gbps)'], - [IFACE_FF_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'], - [IFACE_FF_INFINIBAND_FDR, 'FDR (13.5 Gbps)'], - [IFACE_FF_INFINIBAND_EDR, 'EDR (25 Gbps)'], - [IFACE_FF_INFINIBAND_HDR, 'HDR (50 Gbps)'], - [IFACE_FF_INFINIBAND_NDR, 'NDR (100 Gbps)'], - [IFACE_FF_INFINIBAND_XDR, 'XDR (250 Gbps)'], - ] - ], - [ - 'Serial', - [ - [IFACE_TYPE_T1, 'T1 (1.544 Mbps)'], - [IFACE_TYPE_E1, 'E1 (2.048 Mbps)'], - [IFACE_TYPE_T3, 'T3 (45 Mbps)'], - [IFACE_TYPE_E3, 'E3 (34 Mbps)'], - ] - ], - [ - 'Stacking', - [ - [IFACE_TYPE_STACKWISE, 'Cisco StackWise'], - [IFACE_TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'], - [IFACE_TYPE_FLEXSTACK, 'Cisco FlexStack'], - [IFACE_TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'], - [IFACE_TYPE_JUNIPER_VCP, 'Juniper VCP'], - [IFACE_TYPE_SUMMITSTACK, 'Extreme SummitStack'], - [IFACE_TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'], - [IFACE_TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'], - [IFACE_TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'], - ] - ], - [ - 'Other', - [ - [IFACE_TYPE_KEYSTONE, 'Keystone'], - [IFACE_TYPE_OTHER, 'Other'], - ] - ], -] +# +# Racks +# + +RACK_U_HEIGHT_DEFAULT = 42 + +RACK_ELEVATION_BORDER_WIDTH = 2 +RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30 +RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 220 +RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 22 + + +# +# RearPorts +# + +REARPORT_POSITIONS_MIN = 1 +REARPORT_POSITIONS_MAX = 64 + + +# +# Interfaces +# + +INTERFACE_MTU_MIN = 1 +INTERFACE_MTU_MAX = 32767 # Max value of a signed 16-bit integer VIRTUAL_IFACE_TYPES = [ - IFACE_TYPE_VIRTUAL, - IFACE_TYPE_LAG, + InterfaceTypeChoices.TYPE_VIRTUAL, + InterfaceTypeChoices.TYPE_LAG, ] WIRELESS_IFACE_TYPES = [ - IFACE_TYPE_80211A, - IFACE_TYPE_80211G, - IFACE_TYPE_80211N, - IFACE_TYPE_80211AC, - IFACE_TYPE_80211AD, + InterfaceTypeChoices.TYPE_80211A, + InterfaceTypeChoices.TYPE_80211G, + InterfaceTypeChoices.TYPE_80211N, + InterfaceTypeChoices.TYPE_80211AC, + InterfaceTypeChoices.TYPE_80211AD, ] NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES -IFACE_MODE_ACCESS = 100 -IFACE_MODE_TAGGED = 200 -IFACE_MODE_TAGGED_ALL = 300 -IFACE_MODE_CHOICES = [ - [IFACE_MODE_ACCESS, 'Access'], - [IFACE_MODE_TAGGED, 'Tagged'], - [IFACE_MODE_TAGGED_ALL, 'Tagged All'], -] -# Pass-through port types -PORT_TYPE_8P8C = 1000 -PORT_TYPE_110_PUNCH = 1100 -PORT_TYPE_BNC = 1200 -PORT_TYPE_ST = 2000 -PORT_TYPE_SC = 2100 -PORT_TYPE_SC_APC = 2110 -PORT_TYPE_FC = 2200 -PORT_TYPE_LC = 2300 -PORT_TYPE_LC_APC = 2310 -PORT_TYPE_MTRJ = 2400 -PORT_TYPE_MPO = 2500 -PORT_TYPE_LSH = 2600 -PORT_TYPE_LSH_APC = 2610 -PORT_TYPE_CHOICES = [ - [ - 'Copper', - [ - [PORT_TYPE_8P8C, '8P8C'], - [PORT_TYPE_110_PUNCH, '110 Punch'], - [PORT_TYPE_BNC, 'BNC'], - ], - ], - [ - 'Fiber Optic', - [ - [PORT_TYPE_FC, 'FC'], - [PORT_TYPE_LC, 'LC'], - [PORT_TYPE_LC_APC, 'LC/APC'], - [PORT_TYPE_LSH, 'LSH'], - [PORT_TYPE_LSH_APC, 'LSH/APC'], - [PORT_TYPE_MPO, 'MPO'], - [PORT_TYPE_MTRJ, 'MTRJ'], - [PORT_TYPE_SC, 'SC'], - [PORT_TYPE_SC_APC, 'SC/APC'], - [PORT_TYPE_ST, 'ST'], - ] - ] -] +# +# PowerFeeds +# -# Device statuses -DEVICE_STATUS_OFFLINE = 0 -DEVICE_STATUS_ACTIVE = 1 -DEVICE_STATUS_PLANNED = 2 -DEVICE_STATUS_STAGED = 3 -DEVICE_STATUS_FAILED = 4 -DEVICE_STATUS_INVENTORY = 5 -DEVICE_STATUS_DECOMMISSIONING = 6 -DEVICE_STATUS_CHOICES = [ - [DEVICE_STATUS_ACTIVE, 'Active'], - [DEVICE_STATUS_OFFLINE, 'Offline'], - [DEVICE_STATUS_PLANNED, 'Planned'], - [DEVICE_STATUS_STAGED, 'Staged'], - [DEVICE_STATUS_FAILED, 'Failed'], - [DEVICE_STATUS_INVENTORY, 'Inventory'], - [DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'], -] +POWERFEED_VOLTAGE_DEFAULT = 120 -# Site statuses -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'], -] +POWERFEED_AMPERAGE_DEFAULT = 20 -# Bootstrap CSS classes for device/rack statuses -STATUS_CLASSES = { - 0: 'warning', - 1: 'success', - 2: 'info', - 3: 'primary', - 4: 'danger', - 5: 'default', - 6: 'warning', -} +POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage + + +# +# Cabling and connections +# # Console/power/interface connection statuses -CONNECTION_STATUS_PLANNED = False -CONNECTION_STATUS_CONNECTED = True CONNECTION_STATUS_CHOICES = [ - [CONNECTION_STATUS_PLANNED, 'Planned'], - [CONNECTION_STATUS_CONNECTED, 'Connected'], + [False, 'Not Connected'], + [True, 'Connected'], ] # Cable endpoint types -CABLE_TERMINATION_TYPES = [ - 'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', - 'circuittermination', 'powerfeed', -] - -# Cable types -CABLE_TYPE_CAT3 = 1300 -CABLE_TYPE_CAT5 = 1500 -CABLE_TYPE_CAT5E = 1510 -CABLE_TYPE_CAT6 = 1600 -CABLE_TYPE_CAT6A = 1610 -CABLE_TYPE_CAT7 = 1700 -CABLE_TYPE_DAC_ACTIVE = 1800 -CABLE_TYPE_DAC_PASSIVE = 1810 -CABLE_TYPE_COAXIAL = 1900 -CABLE_TYPE_MMF = 3000 -CABLE_TYPE_MMF_OM1 = 3010 -CABLE_TYPE_MMF_OM2 = 3020 -CABLE_TYPE_MMF_OM3 = 3030 -CABLE_TYPE_MMF_OM4 = 3040 -CABLE_TYPE_SMF = 3500 -CABLE_TYPE_SMF_OS1 = 3510 -CABLE_TYPE_SMF_OS2 = 3520 -CABLE_TYPE_AOC = 3800 -CABLE_TYPE_POWER = 5000 -CABLE_TYPE_CHOICES = ( - ( - 'Copper', ( - (CABLE_TYPE_CAT3, 'CAT3'), - (CABLE_TYPE_CAT5, 'CAT5'), - (CABLE_TYPE_CAT5E, 'CAT5e'), - (CABLE_TYPE_CAT6, 'CAT6'), - (CABLE_TYPE_CAT6A, 'CAT6a'), - (CABLE_TYPE_CAT7, 'CAT7'), - (CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'), - (CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'), - (CABLE_TYPE_COAXIAL, 'Coaxial'), - ), - ), - ( - 'Fiber', ( - (CABLE_TYPE_MMF, 'Multimode Fiber'), - (CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'), - (CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'), - (CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'), - (CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'), - (CABLE_TYPE_SMF, 'Singlemode Fiber'), - (CABLE_TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'), - (CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'), - (CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'), - ), - ), - (CABLE_TYPE_POWER, 'Power'), +CABLE_TERMINATION_MODELS = Q( + Q(app_label='circuits', model__in=( + 'circuittermination', + )) | + Q(app_label='dcim', model__in=( + 'consoleport', + 'consoleserverport', + 'frontport', + 'interface', + 'powerfeed', + 'poweroutlet', + 'powerport', + 'rearport', + )) ) -CABLE_TERMINATION_TYPE_CHOICES = { - # (API endpoint, human-friendly name) - 'consoleport': ('console-ports', 'Console port'), - 'consoleserverport': ('console-server-ports', 'Console server port'), - 'powerport': ('power-ports', 'Power port'), - 'poweroutlet': ('power-outlets', 'Power outlet'), - 'interface': ('interfaces', 'Interface'), - 'frontport': ('front-ports', 'Front panel port'), - 'rearport': ('rear-ports', 'Rear panel port'), -} - COMPATIBLE_TERMINATION_TYPES = { 'consoleport': ['consoleserverport', 'frontport', 'rearport'], 'consoleserverport': ['consoleport', 'frontport', 'rearport'], @@ -466,57 +94,3 @@ COMPATIBLE_TERMINATION_TYPES = { 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'], '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'), -) diff --git a/netbox/dcim/elevations.py b/netbox/dcim/elevations.py new file mode 100644 index 000000000..a3672f60d --- /dev/null +++ b/netbox/dcim/elevations.py @@ -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 diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index 9624ce0a3..3acd0d4a1 100644 --- a/netbox/dcim/fields.py +++ b/netbox/dcim/fields.py @@ -3,14 +3,24 @@ from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from netaddr import AddrFormatError, EUI, mac_unix_expanded +from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN + class ASNField(models.BigIntegerField): description = "32-bit ASN field" default_validators = [ - MinValueValidator(1), - MaxValueValidator(4294967295), + MinValueValidator(BGP_ASN_MIN), + MaxValueValidator(BGP_ASN_MAX), ] + def formfield(self, **kwargs): + defaults = { + 'min_value': BGP_ASN_MIN, + 'max_value': BGP_ASN_MAX, + } + defaults.update(**kwargs) + return super().formfield(**defaults) + class mac_unix_expanded_uppercase(mac_unix_expanded): word_fmt = '%.2X' @@ -22,7 +32,7 @@ class MACAddressField(models.Field): def python_type(self): return EUI - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection): return self.to_python(value) def to_python(self, value): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 933535482..be2adae16 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,9 +1,8 @@ import django_filters from django.contrib.auth.models import User -from django.db.models import Q -from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter, CreatedUpdatedFilterSet -from tenancy.filtersets import TenancyFilterSet +from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet +from tenancy.filters import TenancyFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES from utilities.filters import ( @@ -11,6 +10,7 @@ from utilities.filters import ( TagFilter, TreeNodeMultipleChoiceFilter, ) from virtualization.models import Cluster +from .choices import * from .constants import * from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -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( queryset=Region.objects.all(), label='Parent region (ID)', @@ -38,7 +77,7 @@ class RegionFilter(NameSlugSearchFilterSet, CustomFieldFilterSet): fields = ['id', 'name', 'slug'] -class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): +class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -48,7 +87,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet label='Search', ) status = django_filters.MultipleChoiceFilter( - choices=SITE_STATUS_CHOICES, + choices=SiteStatusChoices, null_value=None ) region_id = TreeNodeMultipleChoiceFilter( @@ -92,7 +131,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet return queryset.filter(qs_filter) -class RackGroupFilter(NameSlugSearchFilterSet): +class RackGroupFilterSet(NameSlugSearchFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='site__region__in', @@ -120,14 +159,14 @@ class RackGroupFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class RackRoleFilter(NameSlugSearchFilterSet): +class RackRoleFilterSet(NameSlugSearchFilterSet): class Meta: model = RackRole fields = ['id', 'name', 'slug', 'color'] -class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): +class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -168,7 +207,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet label='Group', ) status = django_filters.MultipleChoiceFilter( - choices=RACK_STATUS_CHOICES, + choices=RackStatusChoices, null_value=None ) role_id = django_filters.ModelMultipleChoiceFilter( @@ -205,7 +244,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet ) -class RackReservationFilter(TenancyFilterSet): +class RackReservationFilterSet(TenancyFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -266,14 +305,14 @@ class RackReservationFilter(TenancyFilterSet): ) -class ManufacturerFilter(NameSlugSearchFilterSet): +class ManufacturerFilterSet(NameSlugSearchFilterSet): class Meta: model = Manufacturer fields = ['id', 'name', 'slug'] -class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): +class DeviceTypeFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -316,6 +355,10 @@ class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): method='_pass_through_ports', label='Has pass-through ports', ) + device_bays = django_filters.BooleanFilter( + method='_device_bays', + label='Has device bays', + ) tag = TagFilter() class Meta: @@ -355,6 +398,9 @@ class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): rearport_templates__isnull=value ) + def _device_bays(self, queryset, name, value): + return queryset.exclude(device_bay_templates__isnull=value) + class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet): devicetype_id = django_filters.ModelMultipleChoiceFilter( @@ -364,70 +410,70 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet): ) -class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet): +class ConsolePortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = ConsolePortTemplate - fields = ['id', 'name'] + fields = ['id', 'name', 'type'] -class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet): +class ConsoleServerPortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = ConsoleServerPortTemplate - fields = ['id', 'name'] + fields = ['id', 'name', 'type'] -class PowerPortTemplateFilter(DeviceTypeComponentFilterSet): +class PowerPortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: 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: model = PowerOutletTemplate - fields = ['id', 'name', 'feed_leg'] + fields = ['id', 'name', 'type', 'feed_leg'] -class InterfaceTemplateFilter(DeviceTypeComponentFilterSet): +class InterfaceTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = InterfaceTemplate fields = ['id', 'name', 'type', 'mgmt_only'] -class FrontPortTemplateFilter(DeviceTypeComponentFilterSet): +class FrontPortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = FrontPortTemplate fields = ['id', 'name', 'type'] -class RearPortTemplateFilter(DeviceTypeComponentFilterSet): +class RearPortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = RearPortTemplate fields = ['id', 'name', 'type', 'positions'] -class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet): +class DeviceBayTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = DeviceBayTemplate fields = ['id', 'name'] -class DeviceRoleFilter(NameSlugSearchFilterSet): +class DeviceRoleFilterSet(NameSlugSearchFilterSet): class Meta: model = DeviceRole fields = ['id', 'name', 'slug', 'color', 'vm_role'] -class PlatformFilter(NameSlugSearchFilterSet): +class PlatformFilterSet(NameSlugSearchFilterSet): manufacturer_id = django_filters.ModelMultipleChoiceFilter( field_name='manufacturer', queryset=Manufacturer.objects.all(), @@ -445,7 +491,7 @@ class PlatformFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug', 'napalm_driver'] -class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): +class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -532,7 +578,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter label='Device model (slug)', ) status = django_filters.MultipleChoiceFilter( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, null_value=None ) is_full_depth = django_filters.BooleanFilter( @@ -583,6 +629,10 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter method='_pass_through_ports', label='Has pass-through ports', ) + device_bays = django_filters.BooleanFilter( + method='_device_bays', + label='Has device bays', + ) tag = TagFilter() class Meta: @@ -636,17 +686,43 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter rearports__isnull=value ) + def _device_bays(self, queryset, name, value): + return queryset.exclude(device_bays__isnull=value) + class DeviceComponentFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='device__site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='device__site__region__in', + to_field_name='slug', + label='Region (slug)', + ) + site_id = django_filters.ModelMultipleChoiceFilter( + field_name='device__site', + queryset=Site.objects.all(), + label='Site (ID)', + ) + site = django_filters.ModelMultipleChoiceFilter( + field_name='device__site__slug', + queryset=Site.objects.all(), + to_field_name='slug', + label='Site name (slug)', + ) device_id = django_filters.ModelMultipleChoiceFilter( queryset=Device.objects.all(), label='Device (ID)', ) - device = django_filters.ModelChoiceFilter( + device = django_filters.ModelMultipleChoiceFilter( + field_name='device__name', queryset=Device.objects.all(), to_field_name='name', label='Device (name)', @@ -662,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( field_name='cable', lookup_expr='isnull', @@ -674,7 +754,11 @@ class ConsolePortFilter(DeviceComponentFilterSet): 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( field_name='cable', lookup_expr='isnull', @@ -686,7 +770,11 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet): 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( field_name='cable', lookup_expr='isnull', @@ -698,7 +786,11 @@ class PowerPortFilter(DeviceComponentFilterSet): 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( field_name='cable', lookup_expr='isnull', @@ -710,14 +802,13 @@ class PowerOutletFilter(DeviceComponentFilterSet): fields = ['id', 'name', 'feed_leg', 'description', 'connection_status'] -class InterfaceFilter(django_filters.FilterSet): - """ - Not using DeviceComponentFilterSet for Interfaces because we need to check for VirtualChassis membership. - """ +class InterfaceFilterSet(DeviceComponentFilterSet): q = django_filters.CharFilter( method='search', label='Search', ) + # Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis + # members device = MultiValueCharFilter( method='filter_device', field_name='name', @@ -753,7 +844,7 @@ class InterfaceFilter(django_filters.FilterSet): label='Assigned VID' ) type = django_filters.MultipleChoiceFilter( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, null_value=None ) @@ -761,14 +852,6 @@ class InterfaceFilter(django_filters.FilterSet): model = Interface 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): try: devices = Device.objects.filter(**{'{}__in'.format(name): value}) @@ -817,7 +900,7 @@ class InterfaceFilter(django_filters.FilterSet): }.get(value, queryset.none()) -class FrontPortFilter(DeviceComponentFilterSet): +class FrontPortFilterSet(DeviceComponentFilterSet): cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -829,7 +912,7 @@ class FrontPortFilter(DeviceComponentFilterSet): fields = ['id', 'name', 'type', 'description'] -class RearPortFilter(DeviceComponentFilterSet): +class RearPortFilterSet(DeviceComponentFilterSet): cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -841,14 +924,14 @@ class RearPortFilter(DeviceComponentFilterSet): fields = ['id', 'name', 'type', 'positions', 'description'] -class DeviceBayFilter(DeviceComponentFilterSet): +class DeviceBayFilterSet(DeviceComponentFilterSet): class Meta: model = DeviceBay fields = ['id', 'name', 'description'] -class InventoryItemFilter(DeviceComponentFilterSet): +class InventoryItemFilterSet(DeviceComponentFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -919,7 +1002,7 @@ class InventoryItemFilter(DeviceComponentFilterSet): return queryset.filter(qs_filter) -class VirtualChassisFilter(django_filters.FilterSet): +class VirtualChassisFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -973,16 +1056,16 @@ class VirtualChassisFilter(django_filters.FilterSet): return queryset.filter(qs_filter) -class CableFilter(django_filters.FilterSet): +class CableFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', ) type = django_filters.MultipleChoiceFilter( - choices=CABLE_TYPE_CHOICES + choices=CableTypeChoices ) status = django_filters.MultipleChoiceFilter( - choices=CONNECTION_STATUS_CHOICES + choices=CableStatusChoices ) color = django_filters.MultipleChoiceFilter( choices=COLOR_CHOICES @@ -1010,6 +1093,14 @@ class CableFilter(django_filters.FilterSet): method='filter_device', field_name='device__site__slug' ) + tenant_id = MultiValueNumberFilter( + method='filter_device', + field_name='device__tenant_id' + ) + tenant = MultiValueNumberFilter( + method='filter_device', + field_name='device__tenant__slug' + ) class Meta: model = Cable @@ -1028,7 +1119,7 @@ class CableFilter(django_filters.FilterSet): return queryset -class ConsoleConnectionFilter(django_filters.FilterSet): +class ConsoleConnectionFilterSet(django_filters.FilterSet): site = django_filters.CharFilter( method='filter_site', 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( method='filter_site', 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( method='filter_site', 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( field_name='id', lookup_expr='in' @@ -1173,7 +1264,7 @@ class PowerPanelFilter(django_filters.FilterSet): return queryset.filter(qs_filter) -class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): +class PowerFeedFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/dcim/fixtures/dcim.json b/netbox/dcim/fixtures/dcim.json deleted file mode 100644 index ece19a83c..000000000 --- a/netbox/dcim/fixtures/dcim.json +++ /dev/null @@ -1,5732 +0,0 @@ -[ -{ - "model": "dcim.site", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "name": "TEST1", - "slug": "test1", - "facility": "Test Facility", - "asn": 65535, - "physical_address": "555 Test Ave.\r\nTest, NY 55555", - "shipping_address": "", - "comments": "" - } -}, -{ - "model": "dcim.rack", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "name": "A1R1", - "facility_id": "T23A01", - "site": 1, - "group": null, - "u_height": 42, - "comments": "" - } -}, -{ - "model": "dcim.rack", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "name": "A1R2", - "facility_id": "T24A01", - "site": 1, - "group": null, - "u_height": 42, - "comments": "" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 1, - "fields": { - "name": "Juniper", - "slug": "juniper" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 2, - "fields": { - "name": "Opengear", - "slug": "opengear" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 3, - "fields": { - "name": "ServerTech", - "slug": "servertech" - } -}, -{ - "model": "dcim.devicetype", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "MX960", - "slug": "mx960", - "u_height": 16, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "EX9214", - "slug": "ex9214", - "u_height": 16, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 3, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "QFX5100-24Q", - "slug": "qfx5100-24q", - "u_height": 1, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 4, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "QFX5100-48S", - "slug": "qfx5100-48s", - "u_height": 1, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 5, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 2, - "model": "CM4148", - "slug": "cm4148", - "u_height": 1, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 6, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 3, - "model": "CWG-24VYM415C9", - "slug": "cwg-24vym415c9", - "u_height": 0, - "is_full_depth": false - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 1, - "fields": { - "device_type": 1, - "name": "Console (RE0)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 2, - "fields": { - "device_type": 1, - "name": "Console (RE1)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 3, - "fields": { - "device_type": 2, - "name": "Console (RE0)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 4, - "fields": { - "device_type": 2, - "name": "Console (RE1)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 5, - "fields": { - "device_type": 3, - "name": "Console" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 6, - "fields": { - "device_type": 5, - "name": "Console" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 7, - "fields": { - "device_type": 6, - "name": "Serial" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 1, - "fields": { - "device_type": 3, - "name": "Console" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 3, - "fields": { - "device_type": 4, - "name": "Console" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 4, - "fields": { - "device_type": 5, - "name": "Port 1" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 5, - "fields": { - "device_type": 5, - "name": "Port 2" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 6, - "fields": { - "device_type": 5, - "name": "Port 3" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 7, - "fields": { - "device_type": 5, - "name": "Port 4" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 8, - "fields": { - "device_type": 5, - "name": "Port 5" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 9, - "fields": { - "device_type": 5, - "name": "Port 6" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 10, - "fields": { - "device_type": 5, - "name": "Port 7" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 11, - "fields": { - "device_type": 5, - "name": "Port 8" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 12, - "fields": { - "device_type": 5, - "name": "Port 9" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 13, - "fields": { - "device_type": 5, - "name": "Port 10" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 14, - "fields": { - "device_type": 5, - "name": "Port 11" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 15, - "fields": { - "device_type": 5, - "name": "Port 12" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 16, - "fields": { - "device_type": 5, - "name": "Port 13" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 17, - "fields": { - "device_type": 5, - "name": "Port 14" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 18, - "fields": { - "device_type": 5, - "name": "Port 15" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 19, - "fields": { - "device_type": 5, - "name": "Port 16" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 20, - "fields": { - "device_type": 5, - "name": "Port 17" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 21, - "fields": { - "device_type": 5, - "name": "Port 18" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 22, - "fields": { - "device_type": 5, - "name": "Port 19" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 23, - "fields": { - "device_type": 5, - "name": "Port 20" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 24, - "fields": { - "device_type": 5, - "name": "Port 21" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 25, - "fields": { - "device_type": 5, - "name": "Port 22" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 26, - "fields": { - "device_type": 5, - "name": "Port 23" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 27, - "fields": { - "device_type": 5, - "name": "Port 24" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 28, - "fields": { - "device_type": 5, - "name": "Port 25" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 29, - "fields": { - "device_type": 5, - "name": "Port 26" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 30, - "fields": { - "device_type": 5, - "name": "Port 27" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 31, - "fields": { - "device_type": 5, - "name": "Port 28" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 32, - "fields": { - "device_type": 5, - "name": "Port 29" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 33, - "fields": { - "device_type": 5, - "name": "Port 30" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 34, - "fields": { - "device_type": 5, - "name": "Port 31" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 35, - "fields": { - "device_type": 5, - "name": "Port 32" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 36, - "fields": { - "device_type": 5, - "name": "Port 33" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 37, - "fields": { - "device_type": 5, - "name": "Port 34" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 38, - "fields": { - "device_type": 5, - "name": "Port 35" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 39, - "fields": { - "device_type": 5, - "name": "Port 36" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 40, - "fields": { - "device_type": 5, - "name": "Port 37" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 41, - "fields": { - "device_type": 5, - "name": "Port 38" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 42, - "fields": { - "device_type": 5, - "name": "Port 39" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 43, - "fields": { - "device_type": 5, - "name": "Port 40" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 44, - "fields": { - "device_type": 5, - "name": "Port 41" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 45, - "fields": { - "device_type": 5, - "name": "Port 42" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 46, - "fields": { - "device_type": 5, - "name": "Port 43" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 47, - "fields": { - "device_type": 5, - "name": "Port 44" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 48, - "fields": { - "device_type": 5, - "name": "Port 45" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 49, - "fields": { - "device_type": 5, - "name": "Port 46" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 50, - "fields": { - "device_type": 5, - "name": "Port 47" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 51, - "fields": { - "device_type": 5, - "name": "Port 48" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 1, - "fields": { - "device_type": 1, - "name": "PEM0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 2, - "fields": { - "device_type": 1, - "name": "PEM1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 3, - "fields": { - "device_type": 1, - "name": "PEM2" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 4, - "fields": { - "device_type": 1, - "name": "PEM3" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 5, - "fields": { - "device_type": 2, - "name": "PEM0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 6, - "fields": { - "device_type": 2, - "name": "PEM1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 7, - "fields": { - "device_type": 2, - "name": "PEM2" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 8, - "fields": { - "device_type": 2, - "name": "PEM3" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 9, - "fields": { - "device_type": 4, - "name": "PSU0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 11, - "fields": { - "device_type": 3, - "name": "PSU0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 12, - "fields": { - "device_type": 3, - "name": "PSU1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 13, - "fields": { - "device_type": 4, - "name": "PSU1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 14, - "fields": { - "device_type": 5, - "name": "PSU" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 4, - "fields": { - "device_type": 6, - "name": "AA1" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 5, - "fields": { - "device_type": 6, - "name": "AA2" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 6, - "fields": { - "device_type": 6, - "name": "AA3" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 7, - "fields": { - "device_type": 6, - "name": "AA4" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 8, - "fields": { - "device_type": 6, - "name": "AA5" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 9, - "fields": { - "device_type": 6, - "name": "AA6" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 10, - "fields": { - "device_type": 6, - "name": "AA7" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 11, - "fields": { - "device_type": 6, - "name": "AA8" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 12, - "fields": { - "device_type": 6, - "name": "AB1" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 13, - "fields": { - "device_type": 6, - "name": "AB2" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 14, - "fields": { - "device_type": 6, - "name": "AB3" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 15, - "fields": { - "device_type": 6, - "name": "AB4" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 16, - "fields": { - "device_type": 6, - "name": "AB5" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 17, - "fields": { - "device_type": 6, - "name": "AB6" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 18, - "fields": { - "device_type": 6, - "name": "AB7" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 19, - "fields": { - "device_type": 6, - "name": "AB8" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 20, - "fields": { - "device_type": 6, - "name": "AC1" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 21, - "fields": { - "device_type": 6, - "name": "AC2" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 22, - "fields": { - "device_type": 6, - "name": "AC3" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 23, - "fields": { - "device_type": 6, - "name": "AC4" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 24, - "fields": { - "device_type": 6, - "name": "AC5" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 25, - "fields": { - "device_type": 6, - "name": "AC6" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 26, - "fields": { - "device_type": 6, - "name": "AC7" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 27, - "fields": { - "device_type": 6, - "name": "AC8" - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 1, - "fields": { - "device_type": 1, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 2, - "fields": { - "device_type": 1, - "name": "fxp0 (RE1)", - "type": 800, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 3, - "fields": { - "device_type": 1, - "name": "lo0", - "type": 0, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 4, - "fields": { - "device_type": 2, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 5, - "fields": { - "device_type": 2, - "name": "fxp0 (RE1)", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 6, - "fields": { - "device_type": 2, - "name": "lo0", - "type": 0, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 7, - "fields": { - "device_type": 3, - "name": "em0", - "type": 800, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 8, - "fields": { - "device_type": 3, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 9, - "fields": { - "device_type": 3, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 10, - "fields": { - "device_type": 3, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 11, - "fields": { - "device_type": 3, - "name": "et-0/0/3", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 12, - "fields": { - "device_type": 3, - "name": "et-0/0/4", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 13, - "fields": { - "device_type": 3, - "name": "et-0/0/5", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 14, - "fields": { - "device_type": 3, - "name": "et-0/0/6", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 15, - "fields": { - "device_type": 3, - "name": "et-0/0/7", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 16, - "fields": { - "device_type": 3, - "name": "et-0/0/8", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 17, - "fields": { - "device_type": 3, - "name": "et-0/0/9", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 18, - "fields": { - "device_type": 3, - "name": "et-0/0/10", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 19, - "fields": { - "device_type": 3, - "name": "et-0/0/11", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 20, - "fields": { - "device_type": 3, - "name": "et-0/0/12", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 21, - "fields": { - "device_type": 3, - "name": "et-0/0/13", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 22, - "fields": { - "device_type": 3, - "name": "et-0/0/14", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 23, - "fields": { - "device_type": 3, - "name": "et-0/0/15", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 24, - "fields": { - "device_type": 3, - "name": "et-0/0/16", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 25, - "fields": { - "device_type": 3, - "name": "et-0/0/17", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 26, - "fields": { - "device_type": 3, - "name": "et-0/0/18", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 27, - "fields": { - "device_type": 3, - "name": "et-0/0/19", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 28, - "fields": { - "device_type": 3, - "name": "et-0/0/20", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 29, - "fields": { - "device_type": 3, - "name": "et-0/0/21", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 30, - "fields": { - "device_type": 3, - "name": "et-0/0/22", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 31, - "fields": { - "device_type": 3, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 32, - "fields": { - "device_type": 3, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 33, - "fields": { - "device_type": 3, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 34, - "fields": { - "device_type": 3, - "name": "et-0/1/3", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 35, - "fields": { - "device_type": 3, - "name": "et-0/2/0", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 36, - "fields": { - "device_type": 3, - "name": "et-0/2/1", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 37, - "fields": { - "device_type": 3, - "name": "et-0/2/2", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 38, - "fields": { - "device_type": 3, - "name": "et-0/2/3", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 138, - "fields": { - "device_type": 4, - "name": "em0", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 139, - "fields": { - "device_type": 4, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 140, - "fields": { - "device_type": 4, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 141, - "fields": { - "device_type": 4, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 142, - "fields": { - "device_type": 4, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 143, - "fields": { - "device_type": 4, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 144, - "fields": { - "device_type": 4, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 145, - "fields": { - "device_type": 4, - "name": "xe-0/0/6", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 146, - "fields": { - "device_type": 4, - "name": "xe-0/0/7", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 147, - "fields": { - "device_type": 4, - "name": "xe-0/0/8", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 148, - "fields": { - "device_type": 4, - "name": "xe-0/0/9", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 149, - "fields": { - "device_type": 4, - "name": "xe-0/0/10", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 150, - "fields": { - "device_type": 4, - "name": "xe-0/0/11", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 151, - "fields": { - "device_type": 4, - "name": "xe-0/0/12", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 152, - "fields": { - "device_type": 4, - "name": "xe-0/0/13", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 153, - "fields": { - "device_type": 4, - "name": "xe-0/0/14", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 154, - "fields": { - "device_type": 4, - "name": "xe-0/0/15", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 155, - "fields": { - "device_type": 4, - "name": "xe-0/0/16", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 156, - "fields": { - "device_type": 4, - "name": "xe-0/0/17", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 157, - "fields": { - "device_type": 4, - "name": "xe-0/0/18", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 158, - "fields": { - "device_type": 4, - "name": "xe-0/0/19", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 159, - "fields": { - "device_type": 4, - "name": "xe-0/0/20", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 160, - "fields": { - "device_type": 4, - "name": "xe-0/0/21", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 161, - "fields": { - "device_type": 4, - "name": "xe-0/0/22", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 162, - "fields": { - "device_type": 4, - "name": "xe-0/0/23", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 163, - "fields": { - "device_type": 4, - "name": "xe-0/0/24", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 164, - "fields": { - "device_type": 4, - "name": "xe-0/0/25", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 165, - "fields": { - "device_type": 4, - "name": "xe-0/0/26", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 166, - "fields": { - "device_type": 4, - "name": "xe-0/0/27", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 167, - "fields": { - "device_type": 4, - "name": "xe-0/0/28", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 168, - "fields": { - "device_type": 4, - "name": "xe-0/0/29", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 169, - "fields": { - "device_type": 4, - "name": "xe-0/0/30", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 170, - "fields": { - "device_type": 4, - "name": "xe-0/0/31", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 171, - "fields": { - "device_type": 4, - "name": "xe-0/0/32", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 172, - "fields": { - "device_type": 4, - "name": "xe-0/0/33", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 173, - "fields": { - "device_type": 4, - "name": "xe-0/0/34", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 174, - "fields": { - "device_type": 4, - "name": "xe-0/0/35", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 175, - "fields": { - "device_type": 4, - "name": "xe-0/0/36", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 176, - "fields": { - "device_type": 4, - "name": "xe-0/0/37", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 177, - "fields": { - "device_type": 4, - "name": "xe-0/0/38", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 178, - "fields": { - "device_type": 4, - "name": "xe-0/0/39", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 179, - "fields": { - "device_type": 4, - "name": "xe-0/0/40", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 180, - "fields": { - "device_type": 4, - "name": "xe-0/0/41", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 181, - "fields": { - "device_type": 4, - "name": "xe-0/0/42", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 182, - "fields": { - "device_type": 4, - "name": "xe-0/0/43", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 183, - "fields": { - "device_type": 4, - "name": "xe-0/0/44", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 184, - "fields": { - "device_type": 4, - "name": "xe-0/0/45", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 185, - "fields": { - "device_type": 4, - "name": "xe-0/0/46", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 186, - "fields": { - "device_type": 4, - "name": "xe-0/0/47", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 187, - "fields": { - "device_type": 4, - "name": "et-0/0/48", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 188, - "fields": { - "device_type": 4, - "name": "et-0/0/49", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 189, - "fields": { - "device_type": 4, - "name": "et-0/0/50", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 190, - "fields": { - "device_type": 4, - "name": "et-0/0/51", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 191, - "fields": { - "device_type": 5, - "name": "eth0", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 192, - "fields": { - "device_type": 6, - "name": "Net", - "type": 800, - "mgmt_only": true - } -}, -{ - "model": "dcim.devicerole", - "pk": 1, - "fields": { - "name": "Router", - "slug": "router", - "color": "purple" - } -}, -{ - "model": "dcim.devicerole", - "pk": 2, - "fields": { - "name": "Spine Switch", - "slug": "spine-switch", - "color": "green" - } -}, -{ - "model": "dcim.devicerole", - "pk": 3, - "fields": { - "name": "Core Switch", - "slug": "core-switch", - "color": "red" - } -}, -{ - "model": "dcim.devicerole", - "pk": 4, - "fields": { - "name": "Leaf Switch", - "slug": "leaf-switch", - "color": "teal" - } -}, -{ - "model": "dcim.devicerole", - "pk": 5, - "fields": { - "name": "OOB Switch", - "slug": "oob-switch", - "color": "purple" - } -}, -{ - "model": "dcim.devicerole", - "pk": 6, - "fields": { - "name": "PDU", - "slug": "pdu", - "color": "yellow" - } -}, -{ - "model": "dcim.platform", - "pk": 1, - "fields": { - "name": "Juniper Junos", - "slug": "juniper-junos" - } -}, -{ - "model": "dcim.platform", - "pk": 2, - "fields": { - "name": "Opengear", - "slug": "opengear" - } -}, -{ - "model": "dcim.device", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 1, - "device_role": 1, - "platform": 1, - "name": "test1-edge1", - "serial": "5555555555", - "site": 1, - "rack": 1, - "position": 1, - "face": 0, - "status": true, - "primary_ip4": 1, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 2, - "device_role": 3, - "platform": 1, - "name": "test1-core1", - "serial": "", - "site": 1, - "rack": 1, - "position": 17, - "face": 0, - "status": true, - "primary_ip4": 5, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 3, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 3, - "device_role": 2, - "platform": 1, - "name": "test1-spine1", - "serial": "", - "site": 1, - "rack": 1, - "position": 33, - "face": 0, - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 4, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 4, - "device_role": 4, - "platform": 1, - "name": "test1-leaf1", - "serial": "", - "site": 1, - "rack": 1, - "position": 34, - "face": 0, - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 5, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 4, - "device_role": 4, - "platform": 1, - "name": "test1-leaf2", - "serial": "9823478293748", - "site": 1, - "rack": 2, - "position": 34, - "face": 0, - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 6, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 3, - "device_role": 2, - "platform": 1, - "name": "test1-spine2", - "serial": "45649818158", - "site": 1, - "rack": 2, - "position": 33, - "face": 0, - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 7, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 1, - "device_role": 1, - "platform": 1, - "name": "test1-edge2", - "serial": "7567356345", - "site": 1, - "rack": 2, - "position": 1, - "face": 0, - "status": true, - "primary_ip4": 3, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 8, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 2, - "device_role": 3, - "platform": 1, - "name": "test1-core2", - "serial": "67856734534", - "site": 1, - "rack": 2, - "position": 17, - "face": 0, - "status": true, - "primary_ip4": 19, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 9, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 5, - "device_role": 5, - "platform": 2, - "name": "test1-oob1", - "serial": "98273942938", - "site": 1, - "rack": 1, - "position": 42, - "face": 0, - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 11, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 6, - "device_role": 6, - "platform": null, - "name": "test1-pdu1", - "serial": "", - "site": 1, - "rack": 1, - "position": null, - "face": null, - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 12, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 6, - "device_role": 6, - "platform": null, - "name": "test1-pdu2", - "serial": "", - "site": 1, - "rack": 2, - "position": null, - "face": null, - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.consoleport", - "pk": 1, - "fields": { - "device": 1, - "name": "Console (RE0)", - "connected_endpoint": 27, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 2, - "fields": { - "device": 1, - "name": "Console (RE1)", - "connected_endpoint": 38, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 3, - "fields": { - "device": 2, - "name": "Console (RE0)", - "connected_endpoint": 5, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 4, - "fields": { - "device": 2, - "name": "Console (RE1)", - "connected_endpoint": 16, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 5, - "fields": { - "device": 3, - "name": "Console", - "connected_endpoint": 49, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 6, - "fields": { - "device": 4, - "name": "Console", - "connected_endpoint": 48, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 7, - "fields": { - "device": 5, - "name": "Console", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 8, - "fields": { - "device": 6, - "name": "Console", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 9, - "fields": { - "device": 7, - "name": "Console (RE0)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 10, - "fields": { - "device": 7, - "name": "Console (RE1)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 11, - "fields": { - "device": 8, - "name": "Console (RE0)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 12, - "fields": { - "device": 8, - "name": "Console (RE1)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 13, - "fields": { - "device": 9, - "name": "Console", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 15, - "fields": { - "device": 11, - "name": "Serial", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 16, - "fields": { - "device": 12, - "name": "Serial", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 5, - "fields": { - "device": 9, - "name": "Port 1" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 6, - "fields": { - "device": 9, - "name": "Port 10" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 7, - "fields": { - "device": 9, - "name": "Port 11" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 8, - "fields": { - "device": 9, - "name": "Port 12" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 9, - "fields": { - "device": 9, - "name": "Port 13" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 10, - "fields": { - "device": 9, - "name": "Port 14" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 11, - "fields": { - "device": 9, - "name": "Port 15" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 12, - "fields": { - "device": 9, - "name": "Port 16" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 13, - "fields": { - "device": 9, - "name": "Port 17" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 14, - "fields": { - "device": 9, - "name": "Port 18" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 15, - "fields": { - "device": 9, - "name": "Port 19" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 16, - "fields": { - "device": 9, - "name": "Port 2" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 17, - "fields": { - "device": 9, - "name": "Port 20" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 18, - "fields": { - "device": 9, - "name": "Port 21" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 19, - "fields": { - "device": 9, - "name": "Port 22" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 20, - "fields": { - "device": 9, - "name": "Port 23" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 21, - "fields": { - "device": 9, - "name": "Port 24" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 22, - "fields": { - "device": 9, - "name": "Port 25" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 23, - "fields": { - "device": 9, - "name": "Port 26" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 24, - "fields": { - "device": 9, - "name": "Port 27" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 25, - "fields": { - "device": 9, - "name": "Port 28" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 26, - "fields": { - "device": 9, - "name": "Port 29" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 27, - "fields": { - "device": 9, - "name": "Port 3" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 28, - "fields": { - "device": 9, - "name": "Port 30" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 29, - "fields": { - "device": 9, - "name": "Port 31" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 30, - "fields": { - "device": 9, - "name": "Port 32" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 31, - "fields": { - "device": 9, - "name": "Port 33" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 32, - "fields": { - "device": 9, - "name": "Port 34" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 33, - "fields": { - "device": 9, - "name": "Port 35" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 34, - "fields": { - "device": 9, - "name": "Port 36" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 35, - "fields": { - "device": 9, - "name": "Port 37" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 36, - "fields": { - "device": 9, - "name": "Port 38" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 37, - "fields": { - "device": 9, - "name": "Port 39" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 38, - "fields": { - "device": 9, - "name": "Port 4" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 39, - "fields": { - "device": 9, - "name": "Port 40" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 40, - "fields": { - "device": 9, - "name": "Port 41" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 41, - "fields": { - "device": 9, - "name": "Port 42" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 42, - "fields": { - "device": 9, - "name": "Port 43" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 43, - "fields": { - "device": 9, - "name": "Port 44" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 44, - "fields": { - "device": 9, - "name": "Port 45" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 45, - "fields": { - "device": 9, - "name": "Port 46" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 46, - "fields": { - "device": 9, - "name": "Port 47" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 47, - "fields": { - "device": 9, - "name": "Port 48" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 48, - "fields": { - "device": 9, - "name": "Port 5" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 49, - "fields": { - "device": 9, - "name": "Port 6" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 50, - "fields": { - "device": 9, - "name": "Port 7" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 51, - "fields": { - "device": 9, - "name": "Port 8" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 52, - "fields": { - "device": 9, - "name": "Port 9" - } -}, -{ - "model": "dcim.powerport", - "pk": 1, - "fields": { - "device": 1, - "name": "PEM0", - "_connected_poweroutlet": 25, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 2, - "fields": { - "device": 1, - "name": "PEM1", - "_connected_poweroutlet": 49, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 3, - "fields": { - "device": 1, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 4, - "fields": { - "device": 1, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 5, - "fields": { - "device": 2, - "name": "PEM0", - "_connected_poweroutlet": 26, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 6, - "fields": { - "device": 2, - "name": "PEM1", - "_connected_poweroutlet": 50, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 7, - "fields": { - "device": 2, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 8, - "fields": { - "device": 2, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 9, - "fields": { - "device": 4, - "name": "PSU0", - "_connected_poweroutlet": 28, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 10, - "fields": { - "device": 4, - "name": "PSU1", - "_connected_poweroutlet": 52, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 11, - "fields": { - "device": 5, - "name": "PSU0", - "_connected_poweroutlet": 56, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 12, - "fields": { - "device": 5, - "name": "PSU1", - "_connected_poweroutlet": 32, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 13, - "fields": { - "device": 3, - "name": "PSU0", - "_connected_poweroutlet": 27, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 14, - "fields": { - "device": 3, - "name": "PSU1", - "_connected_poweroutlet": 51, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 15, - "fields": { - "device": 7, - "name": "PEM0", - "_connected_poweroutlet": 53, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 16, - "fields": { - "device": 7, - "name": "PEM1", - "_connected_poweroutlet": 29, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 17, - "fields": { - "device": 7, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 18, - "fields": { - "device": 7, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 19, - "fields": { - "device": 8, - "name": "PEM0", - "_connected_poweroutlet": 54, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 20, - "fields": { - "device": 8, - "name": "PEM1", - "_connected_poweroutlet": 30, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 21, - "fields": { - "device": 8, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 22, - "fields": { - "device": 8, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 23, - "fields": { - "device": 6, - "name": "PSU0", - "_connected_poweroutlet": 55, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 24, - "fields": { - "device": 6, - "name": "PSU1", - "_connected_poweroutlet": 31, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 25, - "fields": { - "device": 9, - "name": "PSU", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 25, - "fields": { - "device": 11, - "name": "AA1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 26, - "fields": { - "device": 11, - "name": "AA2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 27, - "fields": { - "device": 11, - "name": "AA3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 28, - "fields": { - "device": 11, - "name": "AA4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 29, - "fields": { - "device": 11, - "name": "AA5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 30, - "fields": { - "device": 11, - "name": "AA6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 31, - "fields": { - "device": 11, - "name": "AA7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 32, - "fields": { - "device": 11, - "name": "AA8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 33, - "fields": { - "device": 11, - "name": "AB1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 34, - "fields": { - "device": 11, - "name": "AB2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 35, - "fields": { - "device": 11, - "name": "AB3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 36, - "fields": { - "device": 11, - "name": "AB4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 37, - "fields": { - "device": 11, - "name": "AB5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 38, - "fields": { - "device": 11, - "name": "AB6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 39, - "fields": { - "device": 11, - "name": "AB7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 40, - "fields": { - "device": 11, - "name": "AB8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 41, - "fields": { - "device": 11, - "name": "AC1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 42, - "fields": { - "device": 11, - "name": "AC2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 43, - "fields": { - "device": 11, - "name": "AC3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 44, - "fields": { - "device": 11, - "name": "AC4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 45, - "fields": { - "device": 11, - "name": "AC5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 46, - "fields": { - "device": 11, - "name": "AC6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 47, - "fields": { - "device": 11, - "name": "AC7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 48, - "fields": { - "device": 11, - "name": "AC8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 49, - "fields": { - "device": 12, - "name": "AA1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 50, - "fields": { - "device": 12, - "name": "AA2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 51, - "fields": { - "device": 12, - "name": "AA3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 52, - "fields": { - "device": 12, - "name": "AA4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 53, - "fields": { - "device": 12, - "name": "AA5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 54, - "fields": { - "device": 12, - "name": "AA6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 55, - "fields": { - "device": 12, - "name": "AA7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 56, - "fields": { - "device": 12, - "name": "AA8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 57, - "fields": { - "device": 12, - "name": "AB1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 58, - "fields": { - "device": 12, - "name": "AB2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 59, - "fields": { - "device": 12, - "name": "AB3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 60, - "fields": { - "device": 12, - "name": "AB4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 61, - "fields": { - "device": 12, - "name": "AB5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 62, - "fields": { - "device": 12, - "name": "AB6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 63, - "fields": { - "device": 12, - "name": "AB7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 64, - "fields": { - "device": 12, - "name": "AB8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 65, - "fields": { - "device": 12, - "name": "AC1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 66, - "fields": { - "device": 12, - "name": "AC2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 67, - "fields": { - "device": 12, - "name": "AC3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 68, - "fields": { - "device": 12, - "name": "AC4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 69, - "fields": { - "device": 12, - "name": "AC5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 70, - "fields": { - "device": 12, - "name": "AC6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 71, - "fields": { - "device": 12, - "name": "AC7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 72, - "fields": { - "device": 12, - "name": "AC8" - } -}, -{ - "model": "dcim.interface", - "pk": 1, - "fields": { - "device": 1, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 2, - "fields": { - "device": 1, - "name": "fxp0 (RE1)", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 3, - "fields": { - "device": 1, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 4, - "fields": { - "device": 1, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "TEST" - } -}, -{ - "model": "dcim.interface", - "pk": 5, - "fields": { - "device": 1, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 6, - "fields": { - "device": 1, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 7, - "fields": { - "device": 1, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 8, - "fields": { - "device": 1, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 9, - "fields": { - "device": 1, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 10, - "fields": { - "device": 2, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 11, - "fields": { - "device": 2, - "name": "fxp0 (RE1)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 12, - "fields": { - "device": 2, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 13, - "fields": { - "device": 3, - "name": "em0", - "mac_address": "00-00-00-AA-BB-CC", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 14, - "fields": { - "device": 3, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 15, - "fields": { - "device": 3, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 16, - "fields": { - "device": 3, - "name": "et-0/0/10", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 17, - "fields": { - "device": 3, - "name": "et-0/0/11", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 18, - "fields": { - "device": 3, - "name": "et-0/0/12", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 19, - "fields": { - "device": 3, - "name": "et-0/0/13", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 20, - "fields": { - "device": 3, - "name": "et-0/0/14", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 21, - "fields": { - "device": 3, - "name": "et-0/0/15", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 22, - "fields": { - "device": 3, - "name": "et-0/0/16", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 23, - "fields": { - "device": 3, - "name": "et-0/0/17", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 24, - "fields": { - "device": 3, - "name": "et-0/0/18", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 25, - "fields": { - "device": 3, - "name": "et-0/0/19", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 26, - "fields": { - "device": 3, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 27, - "fields": { - "device": 3, - "name": "et-0/0/20", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 28, - "fields": { - "device": 3, - "name": "et-0/0/21", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 29, - "fields": { - "device": 3, - "name": "et-0/0/22", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 30, - "fields": { - "device": 3, - "name": "et-0/0/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 31, - "fields": { - "device": 3, - "name": "et-0/0/4", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 32, - "fields": { - "device": 3, - "name": "et-0/0/5", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 33, - "fields": { - "device": 3, - "name": "et-0/0/6", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 34, - "fields": { - "device": 3, - "name": "et-0/0/7", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 35, - "fields": { - "device": 3, - "name": "et-0/0/8", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 36, - "fields": { - "device": 3, - "name": "et-0/0/9", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 37, - "fields": { - "device": 3, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 38, - "fields": { - "device": 3, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 39, - "fields": { - "device": 3, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 40, - "fields": { - "device": 3, - "name": "et-0/1/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 41, - "fields": { - "device": 3, - "name": "et-0/2/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 42, - "fields": { - "device": 3, - "name": "et-0/2/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 43, - "fields": { - "device": 3, - "name": "et-0/2/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 44, - "fields": { - "device": 3, - "name": "et-0/2/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 45, - "fields": { - "device": 4, - "name": "em0", - "type": 1000, - "mac_address": "ff-ee-dd-33-22-11", - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 46, - "fields": { - "device": 4, - "name": "et-0/0/48", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 47, - "fields": { - "device": 4, - "name": "et-0/0/49", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 48, - "fields": { - "device": 4, - "name": "et-0/0/50", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 49, - "fields": { - "device": 4, - "name": "et-0/0/51", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 50, - "fields": { - "device": 4, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 51, - "fields": { - "device": 4, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 52, - "fields": { - "device": 4, - "name": "xe-0/0/10", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 53, - "fields": { - "device": 4, - "name": "xe-0/0/11", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 54, - "fields": { - "device": 4, - "name": "xe-0/0/12", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 55, - "fields": { - "device": 4, - "name": "xe-0/0/13", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 56, - "fields": { - "device": 4, - "name": "xe-0/0/14", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 57, - "fields": { - "device": 4, - "name": "xe-0/0/15", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 58, - "fields": { - "device": 4, - "name": "xe-0/0/16", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 59, - "fields": { - "device": 4, - "name": "xe-0/0/17", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 60, - "fields": { - "device": 4, - "name": "xe-0/0/18", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 61, - "fields": { - "device": 4, - "name": "xe-0/0/19", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 62, - "fields": { - "device": 4, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 63, - "fields": { - "device": 4, - "name": "xe-0/0/20", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 64, - "fields": { - "device": 4, - "name": "xe-0/0/21", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 65, - "fields": { - "device": 4, - "name": "xe-0/0/22", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 66, - "fields": { - "device": 4, - "name": "xe-0/0/23", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 67, - "fields": { - "device": 4, - "name": "xe-0/0/24", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 68, - "fields": { - "device": 4, - "name": "xe-0/0/25", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 69, - "fields": { - "device": 4, - "name": "xe-0/0/26", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 70, - "fields": { - "device": 4, - "name": "xe-0/0/27", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 71, - "fields": { - "device": 4, - "name": "xe-0/0/28", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 72, - "fields": { - "device": 4, - "name": "xe-0/0/29", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 73, - "fields": { - "device": 4, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 74, - "fields": { - "device": 4, - "name": "xe-0/0/30", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 75, - "fields": { - "device": 4, - "name": "xe-0/0/31", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 76, - "fields": { - "device": 4, - "name": "xe-0/0/32", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 77, - "fields": { - "device": 4, - "name": "xe-0/0/33", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 78, - "fields": { - "device": 4, - "name": "xe-0/0/34", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 79, - "fields": { - "device": 4, - "name": "xe-0/0/35", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 80, - "fields": { - "device": 4, - "name": "xe-0/0/36", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 81, - "fields": { - "device": 4, - "name": "xe-0/0/37", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 82, - "fields": { - "device": 4, - "name": "xe-0/0/38", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 83, - "fields": { - "device": 4, - "name": "xe-0/0/39", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 84, - "fields": { - "device": 4, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 85, - "fields": { - "device": 4, - "name": "xe-0/0/40", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 86, - "fields": { - "device": 4, - "name": "xe-0/0/41", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 87, - "fields": { - "device": 4, - "name": "xe-0/0/42", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 88, - "fields": { - "device": 4, - "name": "xe-0/0/43", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 89, - "fields": { - "device": 4, - "name": "xe-0/0/44", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 90, - "fields": { - "device": 4, - "name": "xe-0/0/45", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 91, - "fields": { - "device": 4, - "name": "xe-0/0/46", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 92, - "fields": { - "device": 4, - "name": "xe-0/0/47", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 93, - "fields": { - "device": 4, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 94, - "fields": { - "device": 4, - "name": "xe-0/0/6", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 95, - "fields": { - "device": 4, - "name": "xe-0/0/7", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 96, - "fields": { - "device": 4, - "name": "xe-0/0/8", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 97, - "fields": { - "device": 4, - "name": "xe-0/0/9", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 98, - "fields": { - "device": 5, - "name": "em0", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 99, - "fields": { - "device": 5, - "name": "et-0/0/48", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 100, - "fields": { - "device": 5, - "name": "et-0/0/49", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 101, - "fields": { - "device": 5, - "name": "et-0/0/50", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 102, - "fields": { - "device": 5, - "name": "et-0/0/51", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 103, - "fields": { - "device": 5, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 104, - "fields": { - "device": 5, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 105, - "fields": { - "device": 5, - "name": "xe-0/0/10", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 106, - "fields": { - "device": 5, - "name": "xe-0/0/11", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 107, - "fields": { - "device": 5, - "name": "xe-0/0/12", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 108, - "fields": { - "device": 5, - "name": "xe-0/0/13", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 109, - "fields": { - "device": 5, - "name": "xe-0/0/14", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 110, - "fields": { - "device": 5, - "name": "xe-0/0/15", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 111, - "fields": { - "device": 5, - "name": "xe-0/0/16", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 112, - "fields": { - "device": 5, - "name": "xe-0/0/17", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 113, - "fields": { - "device": 5, - "name": "xe-0/0/18", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 114, - "fields": { - "device": 5, - "name": "xe-0/0/19", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 115, - "fields": { - "device": 5, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 116, - "fields": { - "device": 5, - "name": "xe-0/0/20", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 117, - "fields": { - "device": 5, - "name": "xe-0/0/21", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 118, - "fields": { - "device": 5, - "name": "xe-0/0/22", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 119, - "fields": { - "device": 5, - "name": "xe-0/0/23", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 120, - "fields": { - "device": 5, - "name": "xe-0/0/24", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 121, - "fields": { - "device": 5, - "name": "xe-0/0/25", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 122, - "fields": { - "device": 5, - "name": "xe-0/0/26", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 123, - "fields": { - "device": 5, - "name": "xe-0/0/27", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 124, - "fields": { - "device": 5, - "name": "xe-0/0/28", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 125, - "fields": { - "device": 5, - "name": "xe-0/0/29", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 126, - "fields": { - "device": 5, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 127, - "fields": { - "device": 5, - "name": "xe-0/0/30", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 128, - "fields": { - "device": 5, - "name": "xe-0/0/31", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 129, - "fields": { - "device": 5, - "name": "xe-0/0/32", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 130, - "fields": { - "device": 5, - "name": "xe-0/0/33", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 131, - "fields": { - "device": 5, - "name": "xe-0/0/34", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 132, - "fields": { - "device": 5, - "name": "xe-0/0/35", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 133, - "fields": { - "device": 5, - "name": "xe-0/0/36", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 134, - "fields": { - "device": 5, - "name": "xe-0/0/37", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 135, - "fields": { - "device": 5, - "name": "xe-0/0/38", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 136, - "fields": { - "device": 5, - "name": "xe-0/0/39", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 137, - "fields": { - "device": 5, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 138, - "fields": { - "device": 5, - "name": "xe-0/0/40", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 139, - "fields": { - "device": 5, - "name": "xe-0/0/41", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 140, - "fields": { - "device": 5, - "name": "xe-0/0/42", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 141, - "fields": { - "device": 5, - "name": "xe-0/0/43", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 142, - "fields": { - "device": 5, - "name": "xe-0/0/44", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 143, - "fields": { - "device": 5, - "name": "xe-0/0/45", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 144, - "fields": { - "device": 5, - "name": "xe-0/0/46", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 145, - "fields": { - "device": 5, - "name": "xe-0/0/47", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 146, - "fields": { - "device": 5, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 147, - "fields": { - "device": 5, - "name": "xe-0/0/6", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 148, - "fields": { - "device": 5, - "name": "xe-0/0/7", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 149, - "fields": { - "device": 5, - "name": "xe-0/0/8", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 150, - "fields": { - "device": 5, - "name": "xe-0/0/9", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 151, - "fields": { - "device": 6, - "name": "em0", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 152, - "fields": { - "device": 6, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 153, - "fields": { - "device": 6, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 154, - "fields": { - "device": 6, - "name": "et-0/0/10", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 155, - "fields": { - "device": 6, - "name": "et-0/0/11", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 156, - "fields": { - "device": 6, - "name": "et-0/0/12", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 157, - "fields": { - "device": 6, - "name": "et-0/0/13", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 158, - "fields": { - "device": 6, - "name": "et-0/0/14", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 159, - "fields": { - "device": 6, - "name": "et-0/0/15", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 160, - "fields": { - "device": 6, - "name": "et-0/0/16", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 161, - "fields": { - "device": 6, - "name": "et-0/0/17", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 162, - "fields": { - "device": 6, - "name": "et-0/0/18", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 163, - "fields": { - "device": 6, - "name": "et-0/0/19", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 164, - "fields": { - "device": 6, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 165, - "fields": { - "device": 6, - "name": "et-0/0/20", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 166, - "fields": { - "device": 6, - "name": "et-0/0/21", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 167, - "fields": { - "device": 6, - "name": "et-0/0/22", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 168, - "fields": { - "device": 6, - "name": "et-0/0/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 169, - "fields": { - "device": 6, - "name": "et-0/0/4", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 170, - "fields": { - "device": 6, - "name": "et-0/0/5", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 171, - "fields": { - "device": 6, - "name": "et-0/0/6", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 172, - "fields": { - "device": 6, - "name": "et-0/0/7", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 173, - "fields": { - "device": 6, - "name": "et-0/0/8", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 174, - "fields": { - "device": 6, - "name": "et-0/0/9", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 175, - "fields": { - "device": 6, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 176, - "fields": { - "device": 6, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 177, - "fields": { - "device": 6, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 178, - "fields": { - "device": 6, - "name": "et-0/1/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 179, - "fields": { - "device": 6, - "name": "et-0/2/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 180, - "fields": { - "device": 6, - "name": "et-0/2/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 181, - "fields": { - "device": 6, - "name": "et-0/2/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 182, - "fields": { - "device": 6, - "name": "et-0/2/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 183, - "fields": { - "device": 7, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 184, - "fields": { - "device": 7, - "name": "fxp0 (RE1)", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 185, - "fields": { - "device": 7, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 186, - "fields": { - "device": 8, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 187, - "fields": { - "device": 8, - "name": "fxp0 (RE1)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 188, - "fields": { - "device": 8, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 189, - "fields": { - "device": 2, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 190, - "fields": { - "device": 2, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 191, - "fields": { - "device": 2, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 192, - "fields": { - "device": 2, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 193, - "fields": { - "device": 2, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 194, - "fields": { - "device": 2, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 195, - "fields": { - "device": 8, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 196, - "fields": { - "device": 8, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 197, - "fields": { - "device": 8, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 198, - "fields": { - "device": 8, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 199, - "fields": { - "device": 8, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 200, - "fields": { - "device": 8, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 201, - "fields": { - "device": 2, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 202, - "fields": { - "device": 2, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 203, - "fields": { - "device": 2, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 204, - "fields": { - "device": 2, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 205, - "fields": { - "device": 2, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 206, - "fields": { - "device": 2, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 207, - "fields": { - "device": 8, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 208, - "fields": { - "device": 8, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 209, - "fields": { - "device": 8, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 210, - "fields": { - "device": 8, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 211, - "fields": { - "device": 8, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 212, - "fields": { - "device": 8, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 213, - "fields": { - "device": 7, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 214, - "fields": { - "device": 7, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 215, - "fields": { - "device": 7, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 216, - "fields": { - "device": 7, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 217, - "fields": { - "device": 7, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 218, - "fields": { - "device": 7, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 219, - "fields": { - "device": 9, - "name": "eth0", - "type": 1000, - "mac_address": "44-55-66-77-88-99", - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 221, - "fields": { - "device": 11, - "name": "Net", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 222, - "fields": { - "device": 12, - "name": "Net", - "type": 800, - "mgmt_only": true, - "description": "" - } -} -] diff --git a/netbox/dcim/fixtures/initial_data.json b/netbox/dcim/fixtures/initial_data.json deleted file mode 100644 index 83f79e3a3..000000000 --- a/netbox/dcim/fixtures/initial_data.json +++ /dev/null @@ -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" - } -} -] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 59f72396c..de6650164 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -5,7 +5,6 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.forms.array import SimpleArrayField from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Q from mptt.forms import TreeNodeChoiceField from netaddr import EUI from netaddr.core import AddrFormatError @@ -14,18 +13,21 @@ from timezone_field import TimeZoneFormField from circuits.models import Circuit, Provider from extras.forms import ( - AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm, + LocalConfigContextFilterForm, ) -from ipam.models import IPAddress, VLAN, VLANGroup +from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN +from ipam.models import IPAddress, VLAN from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, - BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm, - ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, - SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES + BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, + DynamicModelMultipleChoiceField, ExpandableNameField, FlexibleModelChoiceField, JSONField, SelectWithPK, + SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) -from virtualization.models import Cluster, ClusterGroup +from virtualization.models import Cluster, ClusterGroup, VirtualMachine +from .choices import * from .constants import * from .models import ( Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, @@ -39,7 +41,7 @@ DEVICE_BY_PK_RE = r'{\d+\}' INTERFACE_MODE_HELP_TEXT = """ Access: One untagged VLAN
Tagged: One untagged VLAN and/or one or more tagged VLANs
-Tagged All: Implies all VLANs are available (w/optional untagged VLAN) +Tagged (All): Implies all VLANs are available (w/optional untagged VLAN) """ @@ -55,6 +57,49 @@ def get_device_by_name_or_pk(name): return device +class DeviceComponentFilterForm(BootstrapMixin, forms.Form): + + field_order = [ + 'q', 'region', 'site' + ] + q = forms.CharField( + required=False, + label='Search' + ) + region = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url='/api/dcim/regions/', + value_field='slug', + filter_for={ + 'site': 'region' + } + ) + ) + site = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + filter_for={ + 'device_id': 'site', + } + ) + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + label='Device', + widget=APISelectMultiple( + api_url='/api/dcim/devices/', + ) + ) + + class InterfaceCommonForm: def clean(self): @@ -65,15 +110,26 @@ class InterfaceCommonForm: tagged_vlans = self.cleaned_data['tagged_vlans'] # Untagged interfaces cannot be assigned tagged VLANs - if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans: + if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans: raise forms.ValidationError({ 'mode': "An access interface cannot have tagged VLANs assigned." }) # Remove all tagged VLAN assignments from "tagged all" interfaces - elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL: + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL: self.cleaned_data['tagged_vlans'] = [] + # Validate tagged VLANs; must be a global VLAN or in the same site + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED: + valid_sites = [None, self.cleaned_data['device'].site] + invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites] + + if invalid_vlans: + raise forms.ValidationError({ + 'tagged_vlans': "The tagged VLANs ({}) must belong to the same site as the interface's parent " + "device/VM, or they must be global".format(', '.join(invalid_vlans)) + }) + class BulkRenameForm(forms.Form): """ @@ -125,19 +181,19 @@ class MACAddressField(forms.Field): # Regions # -class RegionForm(BootstrapMixin, CustomFieldForm): +class RegionForm(BootstrapMixin, CustomFieldModelForm): + parent = TreeNodeChoiceField( + queryset=Region.objects.all(), + required=False, + widget=StaticSelect2() + ) slug = SlugField() class Meta: model = Region - fields = [ + fields = ( 'parent', 'name', 'slug', - ] - widgets = { - 'parent': APISelect( - api_url="/api/dcim/regions/" - ) - } + ) class RegionCSVForm(forms.ModelForm): @@ -172,13 +228,11 @@ class RegionFilterForm(BootstrapMixin, CustomFieldFilterForm): # Sites # -class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): region = TreeNodeChoiceField( queryset=Region.objects.all(), required=False, - widget=APISelect( - api_url="/api/dcim/regions/" - ) + widget=StaticSelect2() ) slug = SlugField() comments = CommentField() @@ -220,9 +274,9 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class SiteCSVForm(forms.ModelForm): +class SiteCSVForm(CustomFieldModelCSVForm): status = CSVChoiceField( - choices=SITE_STATUS_CHOICES, + choices=SiteStatusChoices, required=False, help_text='Operational status' ) @@ -261,7 +315,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor widget=forms.MultipleHiddenInput ) status = forms.ChoiceField( - choices=add_blank_choice(SITE_STATUS_CHOICES), + choices=add_blank_choice(SiteStatusChoices), required=False, initial='', widget=StaticSelect2() @@ -269,11 +323,9 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor region = TreeNodeChoiceField( queryset=Region.objects.all(), required=False, - widget=APISelect( - api_url="/api/dcim/regions/" - ) + widget=StaticSelect2() ) - tenant = forms.ModelChoiceField( + tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False, widget=APISelect( @@ -281,8 +333,8 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ) ) asn = forms.IntegerField( - min_value=1, - max_value=4294967295, + min_value=BGP_ASN_MIN, + max_value=BGP_ASN_MAX, required=False, label='ASN' ) @@ -310,11 +362,11 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): label='Search' ) status = forms.MultipleChoiceField( - choices=SITE_STATUS_CHOICES, + choices=SiteStatusChoices, required=False, widget=StaticSelect2Multiple() ) - region = forms.ModelMultipleChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -323,6 +375,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): value_field="slug", ) ) + tag = TagFilterField(model) # @@ -330,18 +383,19 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): # class RackGroupForm(BootstrapMixin, forms.ModelForm): + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + widget=APISelect( + api_url="/api/dcim/sites/" + ) + ) slug = SlugField() class Meta: model = RackGroup - fields = [ + fields = ( 'site', 'name', 'slug', - ] - widgets = { - 'site': APISelect( - api_url="/api/dcim/sites/" - ) - } + ) class RackGroupCSVForm(forms.ModelForm): @@ -364,7 +418,7 @@ class RackGroupCSVForm(forms.ModelForm): class RackGroupFilterForm(BootstrapMixin, forms.Form): - region = FilterChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -376,9 +430,10 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form): } ) ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", @@ -396,7 +451,7 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm): class Meta: model = RackRole fields = [ - 'name', 'slug', 'color', + 'name', 'slug', 'color', 'description', ] @@ -416,17 +471,30 @@ class RackRoleCSVForm(forms.ModelForm): # Racks # -class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): - group = ChainedModelChoiceField( +class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + widget=APISelect( + api_url="/api/dcim/sites/", + filter_for={ + 'group': 'site_id', + } + ) + ) + group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), - chains=( - ('site', 'site'), - ), required=False, widget=APISelect( api_url='/api/dcim/rack-groups/', ) ) + role = DynamicModelChoiceField( + queryset=RackRole.objects.all(), + required=False, + widget=APISelect( + api_url='/api/dcim/rack-roles/', + ) + ) comments = CommentField() tags = TagField( required=False @@ -445,23 +513,14 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): 'u_height': "Height in rack units", } widgets = { - 'site': APISelect( - api_url="/api/dcim/sites/", - filter_for={ - 'group': 'site_id', - } - ), 'status': StaticSelect2(), - 'role': APISelect( - api_url="/api/dcim/rack-roles/" - ), 'type': StaticSelect2(), 'width': StaticSelect2(), 'outer_unit': StaticSelect2(), } -class RackCSVForm(forms.ModelForm): +class RackCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -484,7 +543,7 @@ class RackCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=RACK_STATUS_CHOICES, + choices=RackStatusChoices, required=False, help_text='Operational status' ) @@ -498,19 +557,16 @@ class RackCSVForm(forms.ModelForm): } ) type = CSVChoiceField( - choices=RACK_TYPE_CHOICES, + choices=RackTypeChoices, required=False, help_text='Rack type' ) width = forms.ChoiceField( - choices=( - (RACK_WIDTH_19IN, '19'), - (RACK_WIDTH_23IN, '23'), - ), + choices=RackWidthChoices, help_text='Rail-to-rail width (in inches)' ) outer_unit = CSVChoiceField( - choices=RACK_DIMENSION_UNIT_CHOICES, + choices=RackDimensionUnitChoices, required=False, help_text='Unit for outer dimensions' ) @@ -557,7 +613,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput ) - site = forms.ModelChoiceField( + site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, widget=APISelect( @@ -567,14 +623,14 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor } ) ) - group = forms.ModelChoiceField( + group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), required=False, widget=APISelect( api_url="/api/dcim/rack-groups", ) ) - tenant = forms.ModelChoiceField( + tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False, widget=APISelect( @@ -582,12 +638,12 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ) ) status = forms.ChoiceField( - choices=add_blank_choice(RACK_STATUS_CHOICES), + choices=add_blank_choice(RackStatusChoices), required=False, initial='', widget=StaticSelect2() ) - role = forms.ModelChoiceField( + role = DynamicModelChoiceField( queryset=RackRole.objects.all(), required=False, widget=APISelect( @@ -604,12 +660,12 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor required=False ) type = forms.ChoiceField( - choices=add_blank_choice(RACK_TYPE_CHOICES), + choices=add_blank_choice(RackTypeChoices), required=False, widget=StaticSelect2() ) width = forms.ChoiceField( - choices=add_blank_choice(RACK_WIDTH_CHOICES), + choices=add_blank_choice(RackWidthChoices), required=False, widget=StaticSelect2() ) @@ -631,12 +687,13 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor min_value=1 ) outer_unit = forms.ChoiceField( - choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES), + choices=add_blank_choice(RackDimensionUnitChoices), required=False, widget=StaticSelect2() ) comments = CommentField( - widget=SmallTextarea + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -652,7 +709,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): required=False, label='Search' ) - region = FilterChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -664,9 +721,10 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): } ) ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", @@ -675,32 +733,57 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): } ) ) - group_id = FilterChoiceField( + group_id = DynamicModelMultipleChoiceField( queryset=RackGroup.objects.prefetch_related( 'site' ), + required=False, label='Rack group', - null_label='-- None --', widget=APISelectMultiple( api_url="/api/dcim/rack-groups/", null_option=True ) ) status = forms.MultipleChoiceField( - choices=RACK_STATUS_CHOICES, + choices=RackStatusChoices, required=False, widget=StaticSelect2Multiple() ) - role = FilterChoiceField( + role = DynamicModelMultipleChoiceField( queryset=RackRole.objects.all(), to_field_name='slug', - null_label='-- None --', + required=False, widget=APISelectMultiple( api_url="/api/dcim/rack-roles/", value_field="slug", null_option=True, ) ) + tag = TagFilterField(model) + + +# +# Rack elevations +# + +class RackElevationFilterForm(RackFilterForm): + field_order = ['q', 'region', 'site', 'group_id', 'id', 'status', 'role', 'tenant_group', 'tenant'] + id = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + label='Rack', + required=False, + widget=APISelectMultiple( + api_url='/api/dcim/racks/', + display_field='display_name', + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Filter the rack field based on the site and group + self.fields['site'].widget.add_filter_for('id', 'site') + self.fields['group_id'].widget.add_filter_for('id', 'group_id') # @@ -758,7 +841,7 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): required=False, widget=StaticSelect2() ) - tenant = forms.ModelChoiceField( + tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False, widget=APISelect( @@ -780,18 +863,19 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): required=False, label='Search' ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", ) ) - group_id = FilterChoiceField( + group_id = DynamicModelMultipleChoiceField( queryset=RackGroup.objects.prefetch_related('site'), + required=False, label='Rack group', - null_label='-- None --', widget=APISelectMultiple( api_url="/api/dcim/rack-groups/", null_option=True, @@ -828,7 +912,13 @@ class ManufacturerCSVForm(forms.ModelForm): # Device types # -class DeviceTypeForm(BootstrapMixin, CustomFieldForm): +class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm): + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + widget=APISelect( + api_url="/api/dcim/manufacturers/", + ) + ) slug = SlugField( slug_source='model' ) @@ -840,40 +930,25 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm): class Meta: model = DeviceType fields = [ - 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments', - 'tags', + 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + 'front_image', 'rear_image', 'comments', 'tags', ] widgets = { - 'manufacturer': APISelect( - api_url="/api/dcim/manufacturers/" - ), 'subdevice_role': StaticSelect2() } -class DeviceTypeCSVForm(forms.ModelForm): +class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm): manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), - required=True, - to_field_name='name', - help_text='Manufacturer name', - error_messages={ - 'invalid_choice': 'Manufacturer not found.', - } - ) - subdevice_role = CSVChoiceField( - choices=SUBDEVICE_ROLE_CHOICES, - required=False, - help_text='Parent/child status' + to_field_name='name' ) class Meta: model = DeviceType - fields = DeviceType.csv_headers - help_texts = { - 'model': 'Model name', - 'slug': 'URL-friendly slug', - } + fields = [ + 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + ] class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -881,11 +956,11 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkE queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput() ) - manufacturer = forms.ModelChoiceField( + manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), required=False, widget=APISelect( - api_url="/api/dcim/manufactureres" + api_url="/api/dcim/manufacturers" ) ) u_height = forms.IntegerField( @@ -908,20 +983,19 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) - manufacturer = FilterChoiceField( + manufacturer = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/manufacturers/", value_field="slug", ) ) - subdevice_role = forms.NullBooleanField( + subdevice_role = forms.MultipleChoiceField( + choices=add_blank_choice(SubdeviceRoleChoices), required=False, - label='Subdevice role', - widget=StaticSelect2( - choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES) - ) + widget=StaticSelect2Multiple() ) console_ports = forms.NullBooleanField( required=False, @@ -965,6 +1039,7 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + tag = TagFilterField(model) # @@ -976,17 +1051,42 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePortTemplate fields = [ - 'device_type', 'name', + 'device_type', 'name', 'type', ] widgets = { 'device_type': forms.HiddenInput(), } -class ConsolePortTemplateCreateForm(ComponentForm): +class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + widget=StaticSelect2() + ) + + +class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConsolePortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = ('type',) class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): @@ -994,17 +1094,42 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPortTemplate fields = [ - 'device_type', 'name', + 'device_type', 'name', 'type', ] widgets = { 'device_type': forms.HiddenInput(), } -class ConsoleServerPortTemplateCreateForm(ComponentForm): +class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + widget=StaticSelect2() + ) + + +class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConsoleServerPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = ('type',) class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): @@ -1012,35 +1137,70 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerPortTemplate fields = [ - 'device_type', 'name', 'maximum_draw', 'allocated_draw', + 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw', ] widgets = { 'device_type': forms.HiddenInput(), } -class PowerPortTemplateCreateForm(ComponentForm): +class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False + ) maximum_draw = forms.IntegerField( min_value=1, required=False, - help_text="Maximum current draw (watts)" + help_text="Maximum power draw (watts)" ) allocated_draw = forms.IntegerField( min_value=1, required=False, - help_text="Allocated current draw (watts)" + help_text="Allocated power draw (watts)" ) +class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False, + widget=StaticSelect2() + ) + maximum_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Maximum power draw (watts)" + ) + allocated_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Allocated power draw (watts)" + ) + + class Meta: + nullable_fields = ('type', 'maximum_draw', 'allocated_draw') + + class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerOutletTemplate fields = [ - 'device_type', 'name', 'power_port', 'feed_leg', + 'device_type', 'name', 'type', 'power_port', 'feed_leg', ] widgets = { 'device_type': forms.HiddenInput(), @@ -1057,28 +1217,60 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): ) -class PowerOutletTemplateCreateForm(ComponentForm): +class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False + ) power_port = forms.ModelChoiceField( queryset=PowerPortTemplate.objects.all(), required=False ) feed_leg = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_LEG_CHOICES), + choices=add_blank_choice(PowerOutletFeedLegChoices), required=False, widget=StaticSelect2() ) def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) # Limit power_port choices to current DeviceType - self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( - device_type=self.parent + device_type = DeviceType.objects.get( + pk=self.initial.get('device_type') or self.data.get('device_type') ) + self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( + device_type=device_type + ) + + +class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerOutletTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False, + widget=StaticSelect2() + ) + feed_leg = forms.ChoiceField( + choices=add_blank_choice(PowerOutletFeedLegChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = ('type', 'feed_leg') class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): @@ -1094,12 +1286,18 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): } -class InterfaceTemplateCreateForm(ComponentForm): +class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) type = forms.ChoiceField( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, widget=StaticSelect2() ) mgmt_only = forms.BooleanField( @@ -1114,7 +1312,7 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm): widget=forms.MultipleHiddenInput() ) type = forms.ChoiceField( - choices=add_blank_choice(IFACE_TYPE_CHOICES), + choices=add_blank_choice(InterfaceTypeChoices), required=False, widget=StaticSelect2() ) @@ -1151,12 +1349,18 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): ) -class FrontPortTemplateCreateForm(ComponentForm): +class FrontPortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2() ) rear_port_set = forms.MultipleChoiceField( @@ -1166,18 +1370,21 @@ class FrontPortTemplateCreateForm(ComponentForm): ) def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + device_type = DeviceType.objects.get( + pk=self.initial.get('device_type') or self.data.get('device_type') + ) + # Determine which rear port positions are occupied. These will be excluded from the list of available mappings. occupied_port_positions = [ (front_port.rear_port_id, front_port.rear_port_position) - for front_port in self.parent.frontport_templates.all() + for front_port in device_type.frontport_templates.all() ] # Populate rear port choices choices = [] - rear_ports = RearPortTemplate.objects.filter(device_type=self.parent) + rear_ports = RearPortTemplate.objects.filter(device_type=device_type) for rear_port in rear_ports: for i in range(1, rear_port.positions + 1): if (rear_port.pk, i) not in occupied_port_positions: @@ -1208,6 +1415,21 @@ class FrontPortTemplateCreateForm(ComponentForm): } +class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=FrontPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = () + + class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1221,22 +1443,43 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class RearPortTemplateCreateForm(ComponentForm): +class RearPortTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2(), ) positions = forms.IntegerField( - min_value=1, - max_value=64, + min_value=REARPORT_POSITIONS_MIN, + max_value=REARPORT_POSITIONS_MAX, initial=1, help_text='The number of front ports which may be mapped to each rear port' ) +class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=RearPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = () + + class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1249,12 +1492,147 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): } -class DeviceBayTemplateCreateForm(ComponentForm): +class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form): + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-types/' + ) + ) name_pattern = ExpandableNameField( label='Name' ) +# TODO: DeviceBayTemplate has no fields suitable for bulk-editing yet +# class DeviceBayTemplateBulkEditForm(BootstrapMixin, BulkEditForm): +# pk = forms.ModelMultipleChoiceField( +# queryset=FrontPortTemplate.objects.all(), +# widget=forms.MultipleHiddenInput() +# ) +# +# class Meta: +# nullable_fields = () + + +# +# Component template import forms +# + +class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): + + def __init__(self, device_type, data=None, *args, **kwargs): + + # Must pass the parent DeviceType on form initialization + data.update({ + 'device_type': device_type.pk, + }) + + super().__init__(data, *args, **kwargs) + + def clean_device_type(self): + + data = self.cleaned_data['device_type'] + + # Limit fields referencing other components to the parent DeviceType + for field_name, field in self.fields.items(): + if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type': + field.queryset = field.queryset.filter(device_type=data) + + return data + + +class ConsolePortTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = ConsolePortTemplate + fields = [ + 'device_type', 'name', 'type', + ] + + +class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = ConsoleServerPortTemplate + fields = [ + 'device_type', 'name', 'type', + ] + + +class PowerPortTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = PowerPortTemplate + fields = [ + 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw', + ] + + +class PowerOutletTemplateImportForm(ComponentTemplateImportForm): + power_port = forms.ModelChoiceField( + queryset=PowerPortTemplate.objects.all(), + to_field_name='name', + required=False + ) + + class Meta: + model = PowerOutletTemplate + fields = [ + 'device_type', 'name', 'type', 'power_port', 'feed_leg', + ] + + +class InterfaceTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=InterfaceTypeChoices.CHOICES + ) + + class Meta: + model = InterfaceTemplate + fields = [ + 'device_type', 'name', 'type', 'mgmt_only', + ] + + +class FrontPortTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=PortTypeChoices.CHOICES + ) + rear_port = forms.ModelChoiceField( + queryset=RearPortTemplate.objects.all(), + to_field_name='name', + required=False + ) + + class Meta: + model = FrontPortTemplate + fields = [ + 'device_type', 'name', 'type', 'rear_port', 'rear_port_position', + ] + + +class RearPortTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=PortTypeChoices.CHOICES + ) + + class Meta: + model = RearPortTemplate + fields = [ + 'device_type', 'name', 'type', 'positions', + ] + + +class DeviceBayTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = DeviceBayTemplate + fields = [ + 'device_type', 'name', + ] + + # # Device roles # @@ -1265,7 +1643,7 @@ class DeviceRoleForm(BootstrapMixin, forms.ModelForm): class Meta: model = DeviceRole fields = [ - 'name', 'slug', 'color', 'vm_role', + 'name', 'slug', 'color', 'vm_role', 'description', ] @@ -1286,6 +1664,13 @@ class DeviceRoleCSVForm(forms.ModelForm): # class PlatformForm(BootstrapMixin, forms.ModelForm): + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/manufacturers/", + ) + ) slug = SlugField( max_length=64 ) @@ -1296,9 +1681,6 @@ class PlatformForm(BootstrapMixin, forms.ModelForm): 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', ] widgets = { - 'manufacturer': APISelect( - api_url="/api/dcim/manufacturers/" - ), 'napalm_args': SmallTextarea(), } @@ -1327,8 +1709,8 @@ class PlatformCSVForm(forms.ModelForm): # Devices # -class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): - site = forms.ModelChoiceField( +class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): + site = DynamicModelChoiceField( queryset=Site.objects.all(), widget=APISelect( api_url="/api/dcim/sites/", @@ -1337,11 +1719,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): } ) ) - rack = ChainedModelChoiceField( + rack = DynamicModelChoiceField( queryset=Rack.objects.all(), - chains=( - ('site', 'site'), - ), required=False, widget=APISelect( api_url='/api/dcim/racks/', @@ -1353,12 +1732,13 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): empty_value=None, help_text="The lowest-numbered unit occupied by the device", widget=APISelect( - api_url='/api/dcim/racks/{{rack}}/units/', + api_url='/api/dcim/racks/{{rack}}/elevation/', disabled_indicator='device' ) ) - manufacturer = forms.ModelChoiceField( + manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), + required=False, widget=APISelect( api_url="/api/dcim/manufacturers/", filter_for={ @@ -1367,18 +1747,30 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): } ) ) - device_type = ChainedModelChoiceField( + device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), - chains=( - ('manufacturer', 'manufacturer'), - ), - label='Device type', widget=APISelect( api_url='/api/dcim/device-types/', display_field='model' ) ) - cluster_group = forms.ModelChoiceField( + device_role = DynamicModelChoiceField( + queryset=DeviceRole.objects.all(), + widget=APISelect( + api_url='/api/dcim/device-roles/' + ) + ) + platform = DynamicModelChoiceField( + queryset=Platform.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/platforms/", + additional_query_params={ + "manufacturer_id": "null" + } + ) + ) + cluster_group = DynamicModelChoiceField( queryset=ClusterGroup.objects.all(), required=False, widget=APISelect( @@ -1391,11 +1783,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): } ) ) - cluster = ChainedModelChoiceField( + cluster = DynamicModelChoiceField( queryset=Cluster.objects.all(), - chains=( - ('group', 'cluster_group'), - ), required=False, widget=APISelect( api_url='/api/virtualization/clusters/', @@ -1427,16 +1816,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): 'position': 'face' } ), - 'device_role': APISelect( - api_url='/api/dcim/device-roles/' - ), 'status': StaticSelect2(), - 'platform': APISelect( - api_url="/api/dcim/platforms/", - additional_query_params={ - "manufacturer_id": "null" - } - ), 'primary_ip4': StaticSelect2(), 'primary_ip6': StaticSelect2(), } @@ -1453,6 +1833,16 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): if instance and instance.cluster is not None: kwargs['initial']['cluster_group'] = instance.cluster.group + if 'device_type' in kwargs['initial'] and 'manufacturer' not in kwargs['initial']: + device_type_id = kwargs['initial']['device_type'] + manufacturer_id = DeviceType.objects.filter(pk=device_type_id).values_list('manufacturer__pk', flat=True).first() + kwargs['initial']['manufacturer'] = manufacturer_id + + if 'cluster' in kwargs['initial'] and 'cluster_group' not in kwargs['initial']: + cluster_id = kwargs['initial']['cluster'] + cluster_group_id = Cluster.objects.filter(pk=cluster_id).values_list('group__pk', flat=True).first() + kwargs['initial']['cluster_group'] = cluster_group_id + super().__init__(*args, **kwargs) if self.instance.pk: @@ -1525,7 +1915,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.initial['rack'] = self.instance.parent_bay.device.rack_id -class BaseDeviceCSVForm(forms.ModelForm): +class BaseDeviceCSVForm(CustomFieldModelCSVForm): device_role = forms.ModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', @@ -1564,7 +1954,7 @@ class BaseDeviceCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, help_text='Operational status' ) @@ -1608,7 +1998,7 @@ class DeviceCSVForm(BaseDeviceCSVForm): help_text='Name of parent rack' ) face = CSVChoiceField( - choices=RACK_FACE_CHOICES, + choices=DeviceFaceChoices, required=False, help_text='Mounted rack face' ) @@ -1700,31 +2090,29 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF queryset=Device.objects.all(), widget=forms.MultipleHiddenInput() ) - device_type = forms.ModelChoiceField( + device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), required=False, - label='Type', widget=APISelect( api_url="/api/dcim/device-types/", display_field='display_name' ) ) - device_role = forms.ModelChoiceField( + device_role = DynamicModelChoiceField( queryset=DeviceRole.objects.all(), required=False, - label='Role', widget=APISelect( api_url="/api/dcim/device-roles/" ) ) - tenant = forms.ModelChoiceField( + tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), required=False, widget=APISelect( api_url="/api/tenancy/tenants/" ) ) - platform = forms.ModelChoiceField( + platform = DynamicModelChoiceField( queryset=Platform.objects.all(), required=False, widget=APISelect( @@ -1732,9 +2120,8 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ) ) status = forms.ChoiceField( - choices=add_blank_choice(DEVICE_STATUS_CHOICES), + choices=add_blank_choice(DeviceStatusChoices), required=False, - initial='', widget=StaticSelect2() ) serial = forms.CharField( @@ -1752,14 +2139,14 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm): model = Device field_order = [ - 'q', 'region', 'site', 'group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant', + 'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant', 'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip', ] q = forms.CharField( required=False, label='Search' ) - region = FilterChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -1771,22 +2158,22 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt } ) ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", filter_for={ - 'group_id': 'site', + 'rack_group_id': 'site', 'rack_id': 'site', } ) ) - group_id = FilterChoiceField( - queryset=RackGroup.objects.prefetch_related( - 'site' - ), + rack_group_id = DynamicModelMultipleChoiceField( + queryset=RackGroup.objects.all(), + required=False, label='Rack group', widget=APISelectMultiple( api_url="/api/dcim/rack-groups/", @@ -1795,25 +2182,27 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt } ) ) - rack_id = FilterChoiceField( + rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), + required=False, label='Rack', - null_label='-- None --', widget=APISelectMultiple( api_url="/api/dcim/racks/", null_option=True, ) ) - role = FilterChoiceField( + role = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/device-roles/", value_field="slug", ) ) - manufacturer_id = FilterChoiceField( + manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), + required=False, label='Manufacturer', widget=APISelectMultiple( api_url="/api/dcim/manufacturers/", @@ -1822,20 +2211,19 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt } ) ) - device_type_id = FilterChoiceField( - queryset=DeviceType.objects.prefetch_related( - 'manufacturer' - ), + device_type_id = DynamicModelMultipleChoiceField( + queryset=DeviceType.objects.all(), + required=False, label='Model', widget=APISelectMultiple( api_url="/api/dcim/device-types/", display_field="model", ) ) - platform = FilterChoiceField( + platform = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), to_field_name='slug', - null_label='-- None --', + required=False, widget=APISelectMultiple( api_url="/api/dcim/platforms/", value_field="slug", @@ -1843,7 +2231,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt ) ) status = forms.MultipleChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, required=False, widget=StaticSelect2Multiple() ) @@ -1907,6 +2295,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + tag = TagFilterField(model) # @@ -1925,7 +2314,7 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): type = forms.ChoiceField( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, widget=StaticSelect2() ) enabled = forms.BooleanField( @@ -1934,8 +2323,8 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): ) mtu = forms.IntegerField( required=False, - min_value=1, - max_value=32767, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, label='MTU' ) mgmt_only = forms.BooleanField( @@ -1952,6 +2341,12 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): # Console ports # + +class ConsolePortFilterForm(DeviceComponentFilterForm): + model = ConsolePort + tag = TagFilterField(model) + + class ConsolePortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -1960,17 +2355,28 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePort fields = [ - 'device', 'name', 'description', 'tags', + 'device', 'name', 'type', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), } -class ConsolePortCreateForm(ComponentForm): +class ConsolePortCreateForm(BootstrapMixin, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) description = forms.CharField( max_length=100, required=False @@ -1980,10 +2386,52 @@ class ConsolePortCreateForm(ComponentForm): ) +class ConsolePortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConsolePort.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = ( + 'description', + ) + + +class ConsolePortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + + class Meta: + model = ConsolePort + fields = ConsolePort.csv_headers + + # # Console server ports # + +class ConsoleServerPortFilterForm(DeviceComponentFilterForm): + model = ConsoleServerPort + tag = TagFilterField(model) + + class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -1992,17 +2440,28 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPort fields = [ - 'device', 'name', 'description', 'tags', + 'device', 'name', 'type', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), } -class ConsoleServerPortCreateForm(ComponentForm): +class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) description = forms.CharField( max_length=100, required=False @@ -2017,6 +2476,11 @@ class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditF queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput() ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) description = forms.CharField( max_length=100, required=False @@ -2042,10 +2506,31 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): ) +class ConsoleServerPortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + + class Meta: + model = ConsoleServerPort + fields = ConsoleServerPort.csv_headers + + # # Power ports # + +class PowerPortFilterForm(DeviceComponentFilterForm): + model = PowerPort + tag = TagFilterField(model) + + class PowerPortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -2054,17 +2539,28 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerPort fields = [ - 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'tags', + 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), } -class PowerPortCreateForm(ComponentForm): +class PowerPortCreateForm(BootstrapMixin, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False, + widget=StaticSelect2() + ) maximum_draw = forms.IntegerField( min_value=1, required=False, @@ -2084,10 +2580,62 @@ class PowerPortCreateForm(ComponentForm): ) +class PowerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerPort.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False, + widget=StaticSelect2() + ) + maximum_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Maximum draw in watts" + ) + allocated_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Allocated draw in watts" + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = ( + 'description', + ) + + +class PowerPortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + + class Meta: + model = PowerPort + fields = PowerPort.csv_headers + + # # Power outlets # + +class PowerOutletFilterForm(DeviceComponentFilterForm): + model = PowerOutlet + tag = TagFilterField(model) + + class PowerOutletForm(BootstrapMixin, forms.ModelForm): power_port = forms.ModelChoiceField( queryset=PowerPort.objects.all(), @@ -2100,7 +2648,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerOutlet fields = [ - 'device', 'name', 'power_port', 'feed_leg', 'description', 'tags', + 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -2116,16 +2664,27 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm): ) -class PowerOutletCreateForm(ComponentForm): +class PowerOutletCreateForm(BootstrapMixin, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False, + widget=StaticSelect2() + ) power_port = forms.ModelChoiceField( queryset=PowerPort.objects.all(), required=False ) feed_leg = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_LEG_CHOICES), + choices=add_blank_choice(PowerOutletFeedLegChoices), required=False ) description = forms.CharField( @@ -2137,11 +2696,63 @@ class PowerOutletCreateForm(ComponentForm): ) def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Limit power_port choices to those on the parent device - self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent) + # Limit power_port queryset to PowerPorts which belong to the parent Device + device = Device.objects.get( + pk=self.initial.get('device') or self.data.get('device') + ) + self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) + + +class PowerOutletCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + power_port = FlexibleModelChoiceField( + queryset=PowerPort.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of Power Port', + error_messages={ + 'invalid_choice': 'Power Port not found.', + } + ) + feed_leg = CSVChoiceField( + choices=PowerOutletFeedLegChoices, + required=False, + ) + + class Meta: + model = PowerOutlet + fields = PowerOutlet.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit PowerPort choices to those belonging to this device (or VC master) + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['power_port'].queryset = PowerPort.objects.filter( + device__in=[device, device.get_vc_master()] + ) + else: + self.fields['power_port'].queryset = PowerPort.objects.none() class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): @@ -2149,8 +2760,18 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput() ) + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + required=False, + disabled=True, + widget=forms.HiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False + ) feed_leg = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_LEG_CHOICES), + choices=add_blank_choice(PowerOutletFeedLegChoices), required=False, ) power_port = forms.ModelChoiceField( @@ -2164,14 +2785,19 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class Meta: nullable_fields = [ - 'feed_leg', 'power_port', 'description', + 'type', 'feed_leg', 'power_port', 'description', ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Limit power_port queryset to PowerPorts which belong to the parent Device - self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent_obj) + if 'device' in self.initial: + device = Device.objects.filter(pk=self.initial['device']).first() + self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) + else: + self.fields['power_port'].choices = () + self.fields['power_port'].widget.attrs['disabled'] = True class PowerOutletBulkRenameForm(BulkRenameForm): @@ -2192,26 +2818,45 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): # Interfaces # + +class InterfaceFilterForm(DeviceComponentFilterForm): + model = Interface + enabled = forms.NullBooleanField( + required=False, + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + tag = TagFilterField(model) + + class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): - untagged_vlan = forms.ModelChoiceField( + untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, + label='Untagged VLAN', widget=APISelect( api_url="/api/ipam/vlans/", display_field='display_name', - full=True + full=True, + additional_query_params={ + 'site_id': 'null', + }, ) ) - tagged_vlans = forms.ModelMultipleChoiceField( + tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, + label='Tagged VLANs', widget=APISelectMultiple( api_url="/api/ipam/vlans/", display_field='display_name', - full=True + full=True, + additional_query_params={ + 'site_id': 'null', + }, ) ) - tags = TagField( required=False ) @@ -2238,59 +2883,39 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Limit LAG choices to interfaces belonging to this device (or VC master) if self.is_bound: device = Device.objects.get(pk=self.data['device']) - self.fields['lag'].queryset = Interface.objects.filter( - device__in=[device, device.get_vc_master()], type=IFACE_TYPE_LAG - ) else: device = self.instance.device - self.fields['lag'].queryset = Interface.objects.filter( - device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG - ) - # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site - vlan_choices = [] - global_vlans = VLAN.objects.filter(site=None, group=None) - vlan_choices.append( - ('Global', [(vlan.pk, vlan) for vlan in global_vlans]) + # Limit LAG choices to interfaces belonging to this device (or VC master) + self.fields['lag'].queryset = Interface.objects.filter( + device__in=[device, device.get_vc_master()], + type=InterfaceTypeChoices.TYPE_LAG ) - for group in VLANGroup.objects.filter(site=None): - global_group_vlans = VLAN.objects.filter(group=group) - vlan_choices.append( - (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) - ) - site = getattr(self.instance.parent, 'site', None) - if site is not None: - - # Add non-grouped site VLANs - site_vlans = VLAN.objects.filter(site=site, group=None) - vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans])) - - # Add grouped site VLANs - for group in VLANGroup.objects.filter(site=site): - site_group_vlans = VLAN.objects.filter(group=group) - vlan_choices.append(( - '{} / {}'.format(group.site.name, group.name), - [(vlan.pk, vlan) for vlan in site_group_vlans] - )) - - self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices - self.fields['tagged_vlans'].choices = vlan_choices + # Add current site to VLANs query params + self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk) + self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk) -class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): +class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) type = forms.ChoiceField( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, widget=StaticSelect2(), ) enabled = forms.BooleanField( - required=False + required=False, + initial=True ) lag = forms.ModelChoiceField( queryset=Interface.objects.all(), @@ -2300,8 +2925,8 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): ) mtu = forms.IntegerField( required=False, - min_value=1, - max_value=32767, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, label='MTU' ) mac_address = forms.CharField( @@ -2318,86 +2943,135 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): required=False ) mode = forms.ChoiceField( - choices=add_blank_choice(IFACE_MODE_CHOICES), + choices=add_blank_choice(InterfaceModeChoices), required=False, widget=StaticSelect2(), ) tags = TagField( required=False ) - untagged_vlan = forms.ModelChoiceField( + untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, widget=APISelect( api_url="/api/ipam/vlans/", display_field='display_name', - full=True + full=True, + additional_query_params={ + 'site_id': 'null', + }, ) ) - tagged_vlans = forms.ModelMultipleChoiceField( + tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, widget=APISelectMultiple( api_url="/api/ipam/vlans/", display_field='display_name', - full=True + full=True, + additional_query_params={ + 'site_id': 'null', + }, ) ) def __init__(self, *args, **kwargs): - - # Set interfaces enabled by default - kwargs['initial'] = kwargs.get('initial', {}).copy() - kwargs['initial'].update({'enabled': True}) - super().__init__(*args, **kwargs) - # Limit LAG choices to interfaces belonging to this device (or its VC master) - if self.parent is not None: + # Limit LAG choices to interfaces which belong to the parent device (or VC master) + device = Device.objects.get( + pk=self.initial.get('device') or self.data.get('device') + ) + self.fields['lag'].queryset = Interface.objects.filter( + device__in=[device, device.get_vc_master()], + type=InterfaceTypeChoices.TYPE_LAG + ) + + # Add current site to VLANs query params + self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk) + self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk) + + +class InterfaceCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + virtual_machine = FlexibleModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of virtual machine', + error_messages={ + 'invalid_choice': 'Virtual machine not found.', + } + ) + lag = FlexibleModelChoiceField( + queryset=Interface.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of LAG interface', + error_messages={ + 'invalid_choice': 'LAG interface not found.', + } + ) + type = CSVChoiceField( + choices=InterfaceTypeChoices, + ) + mode = CSVChoiceField( + choices=InterfaceModeChoices, + required=False, + ) + + class Meta: + model = Interface + fields = Interface.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit LAG choices to interfaces belonging to this device (or VC master) + if self.is_bound and 'device' in self.data: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + device = self.instance.device + + if device: self.fields['lag'].queryset = Interface.objects.filter( - device__in=[self.parent, self.parent.get_vc_master()], type=IFACE_TYPE_LAG + device__in=[device, device.get_vc_master()], type=InterfaceTypeChoices.TYPE_LAG ) else: self.fields['lag'].queryset = Interface.objects.none() - # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site - vlan_choices = [] - global_vlans = VLAN.objects.filter(site=None, group=None) - vlan_choices.append( - ('Global', [(vlan.pk, vlan) for vlan in global_vlans]) - ) - for group in VLANGroup.objects.filter(site=None): - global_group_vlans = VLAN.objects.filter(group=group) - vlan_choices.append( - (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) - ) - - site = getattr(self.parent, 'site', None) - if site is not None: - - # Add non-grouped site VLANs - site_vlans = VLAN.objects.filter(site=site, group=None) - vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans])) - - # Add grouped site VLANs - for group in VLANGroup.objects.filter(site=site): - site_group_vlans = VLAN.objects.filter(group=group) - vlan_choices.append(( - '{} / {}'.format(group.site.name, group.name), - [(vlan.pk, vlan) for vlan in site_group_vlans] - )) - - self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices - self.fields['tagged_vlans'].choices = vlan_choices + def clean_enabled(self): + # Make sure enabled is True when it's not included in the uploaded data + if 'enabled' not in self.data: + return True + else: + return self.cleaned_data['enabled'] -class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput() ) + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + required=False, + disabled=True, + widget=forms.HiddenInput() + ) type = forms.ChoiceField( - choices=add_blank_choice(IFACE_TYPE_CHOICES), + choices=add_blank_choice(InterfaceTypeChoices), required=False, widget=StaticSelect2() ) @@ -2417,8 +3091,8 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo ) mtu = forms.IntegerField( required=False, - min_value=1, - max_value=32767, + min_value=INTERFACE_MTU_MIN, + max_value=INTERFACE_MTU_MAX, label='MTU' ) mgmt_only = forms.NullBooleanField( @@ -2431,26 +3105,32 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo required=False ) mode = forms.ChoiceField( - choices=add_blank_choice(IFACE_MODE_CHOICES), + choices=add_blank_choice(InterfaceModeChoices), required=False, widget=StaticSelect2() ) - untagged_vlan = forms.ModelChoiceField( + untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, widget=APISelect( api_url="/api/ipam/vlans/", display_field='display_name', - full=True + full=True, + additional_query_params={ + 'site_id': 'null', + }, ) ) - tagged_vlans = forms.ModelMultipleChoiceField( + tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, widget=APISelectMultiple( api_url="/api/ipam/vlans/", display_field='display_name', - full=True + full=True, + additional_query_params={ + 'site_id': 'null', + }, ) ) @@ -2463,44 +3143,31 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo super().__init__(*args, **kwargs) # Limit LAG choices to interfaces which belong to the parent device (or VC master) - device = self.parent_obj - if device is not None: + if 'device' in self.initial: + device = Device.objects.filter(pk=self.initial['device']).first() self.fields['lag'].queryset = Interface.objects.filter( device__in=[device, device.get_vc_master()], - type=IFACE_TYPE_LAG + type=InterfaceTypeChoices.TYPE_LAG ) + + # Add current site to VLANs query params + self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk) + self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk) else: - self.fields['lag'].choices = [] + self.fields['lag'].choices = () + self.fields['lag'].widget.attrs['disabled'] = True - # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site - vlan_choices = [] - global_vlans = VLAN.objects.filter(site=None, group=None) - vlan_choices.append( - ('Global', [(vlan.pk, vlan) for vlan in global_vlans]) - ) - for group in VLANGroup.objects.filter(site=None): - global_group_vlans = VLAN.objects.filter(group=group) - vlan_choices.append( - (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) - ) - if self.parent_obj is not None: - site = getattr(self.parent_obj, 'site', None) - if site is not None: + def clean(self): - # Add non-grouped site VLANs - site_vlans = VLAN.objects.filter(site=site, group=None) - vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans])) + # Untagged interfaces cannot be assigned tagged VLANs + if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']: + raise forms.ValidationError({ + 'mode': "An access interface cannot have tagged VLANs assigned." + }) - # Add grouped site VLANs - for group in VLANGroup.objects.filter(site=site): - site_group_vlans = VLAN.objects.filter(group=group) - vlan_choices.append(( - '{} / {}'.format(group.site.name, group.name), - [(vlan.pk, vlan) for vlan in site_group_vlans] - )) - - self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices - self.fields['tagged_vlans'].choices = vlan_choices + # Remove all tagged VLAN assignments from "tagged all" interfaces + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL: + self.cleaned_data['tagged_vlans'] = [] class InterfaceBulkRenameForm(BulkRenameForm): @@ -2521,6 +3188,11 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): # Front pass-through ports # +class FrontPortFilterForm(DeviceComponentFilterForm): + model = FrontPort + tag = TagFilterField(model) + + class FrontPortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -2548,12 +3220,18 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm): # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic -class FrontPortCreateForm(ComponentForm): +class FrontPortCreateForm(BootstrapMixin, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2(), ) rear_port_set = forms.MultipleChoiceField( @@ -2568,15 +3246,20 @@ class FrontPortCreateForm(ComponentForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Determine which rear port positions are occupied. These will be excluded from the list of available mappings. + device = Device.objects.get( + pk=self.initial.get('device') or self.data.get('device') + ) + + # Determine which rear port positions are occupied. These will be excluded from the list of available + # mappings. occupied_port_positions = [ (front_port.rear_port_id, front_port.rear_port_position) - for front_port in self.parent.frontports.all() + for front_port in device.frontports.all() ] # Populate rear port choices choices = [] - rear_ports = RearPort.objects.filter(device=self.parent) + rear_ports = RearPort.objects.filter(device=device) for rear_port in rear_ports: for i in range(1, rear_port.positions + 1): if (rear_port.pk, i) not in occupied_port_positions: @@ -2607,13 +3290,61 @@ class FrontPortCreateForm(ComponentForm): } +class FrontPortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + rear_port = FlexibleModelChoiceField( + queryset=RearPort.objects.all(), + to_field_name='name', + help_text='Name or ID of Rear Port', + error_messages={ + 'invalid_choice': 'Rear Port not found.', + } + ) + type = CSVChoiceField( + choices=PortTypeChoices, + ) + + class Meta: + model = FrontPort + fields = FrontPort.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit RearPort choices to those belonging to this device (or VC master) + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['rear_port'].queryset = RearPort.objects.filter( + device__in=[device, device.get_vc_master()] + ) + else: + self.fields['rear_port'].queryset = RearPort.objects.none() + + class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=FrontPort.objects.all(), widget=forms.MultipleHiddenInput() ) type = forms.ChoiceField( - choices=add_blank_choice(PORT_TYPE_CHOICES), + choices=add_blank_choice(PortTypeChoices), required=False, widget=StaticSelect2() ) @@ -2646,6 +3377,11 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): # Rear pass-through ports # +class RearPortFilterForm(DeviceComponentFilterForm): + model = RearPort + tag = TagFilterField(model) + + class RearPortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -2662,17 +3398,23 @@ class RearPortForm(BootstrapMixin, forms.ModelForm): } -class RearPortCreateForm(ComponentForm): +class RearPortCreateForm(BootstrapMixin, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2(), ) positions = forms.IntegerField( - min_value=1, - max_value=64, + min_value=REARPORT_POSITIONS_MIN, + max_value=REARPORT_POSITIONS_MAX, initial=1, help_text='The number of front ports which may be mapped to each rear port' ) @@ -2681,13 +3423,31 @@ class RearPortCreateForm(ComponentForm): ) +class RearPortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + type = CSVChoiceField( + choices=PortTypeChoices, + ) + + class Meta: + model = RearPort + fields = RearPort.csv_headers + + class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RearPort.objects.all(), widget=forms.MultipleHiddenInput() ) type = forms.ChoiceField( - choices=add_blank_choice(PORT_TYPE_CHOICES), + choices=add_blank_choice(PortTypeChoices), required=False, widget=StaticSelect2() ) @@ -2720,11 +3480,11 @@ class RearPortBulkDisconnectForm(ConfirmationForm): # Cables # -class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): +class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm): """ Base form for connecting a Cable to a Device component """ - termination_b_site = forms.ModelChoiceField( + termination_b_site = DynamicModelChoiceField( queryset=Site.objects.all(), label='Site', required=False, @@ -2736,11 +3496,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFo } ) ) - termination_b_rack = ChainedModelChoiceField( + termination_b_rack = DynamicModelChoiceField( queryset=Rack.objects.all(), - chains=( - ('site', 'termination_b_site'), - ), label='Rack', required=False, widget=APISelect( @@ -2753,12 +3510,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFo } ) ) - termination_b_device = ChainedModelChoiceField( + termination_b_device = DynamicModelChoiceField( queryset=Device.objects.all(), - chains=( - ('site', 'termination_b_site'), - ('rack', 'termination_b_rack'), - ), label='Device', required=False, widget=APISelect( @@ -2776,6 +3529,11 @@ class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFo 'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', ] + widgets = { + 'status': StaticSelect2, + 'type': StaticSelect2, + 'length_unit': StaticSelect2, + } class ConnectCableToConsolePortForm(ConnectCableToDeviceForm): @@ -2851,10 +3609,11 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm): ) -class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): - termination_b_provider = forms.ModelChoiceField( +class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm): + termination_b_provider = DynamicModelChoiceField( queryset=Provider.objects.all(), label='Provider', + required=False, widget=APISelect( api_url='/api/circuits/providers/', filter_for={ @@ -2862,7 +3621,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, f } ) ) - termination_b_site = forms.ModelChoiceField( + termination_b_site = DynamicModelChoiceField( queryset=Site.objects.all(), label='Site', required=False, @@ -2873,11 +3632,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, f } ) ) - termination_b_circuit = ChainedModelChoiceField( + termination_b_circuit = DynamicModelChoiceField( queryset=Circuit.objects.all(), - chains=( - ('provider', 'termination_b_provider'), - ), label='Circuit', widget=APISelect( api_url='/api/circuits/circuits/', @@ -2892,7 +3648,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, f widget=APISelect( api_url='/api/circuits/circuit-terminations/', disabled_indicator='cable', - display_field='term_side' + display_field='term_side', + full=True ) ) @@ -2904,10 +3661,11 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, f ] -class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): - termination_b_site = forms.ModelChoiceField( +class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): + termination_b_site = DynamicModelChoiceField( queryset=Site.objects.all(), label='Site', + required=False, widget=APISelect( api_url='/api/dcim/sites/', display_field='cid', @@ -2917,12 +3675,9 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.Mode } ) ) - termination_b_rackgroup = ChainedModelChoiceField( + termination_b_rackgroup = DynamicModelChoiceField( queryset=RackGroup.objects.all(), label='Rack Group', - chains=( - ('site', 'termination_b_site'), - ), required=False, widget=APISelect( api_url='/api/dcim/rack-groups/', @@ -2932,13 +3687,10 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.Mode } ) ) - termination_b_powerpanel = ChainedModelChoiceField( + termination_b_powerpanel = DynamicModelChoiceField( queryset=PowerPanel.objects.all(), - chains=( - ('site', 'termination_b_site'), - ('rack_group', 'termination_b_rackgroup'), - ), label='Power Panel', + required=False, widget=APISelect( api_url='/api/dcim/power-panels/', filter_for={ @@ -2968,6 +3720,11 @@ class CableForm(BootstrapMixin, forms.ModelForm): fields = [ 'type', 'status', 'label', 'color', 'length', 'length_unit', ] + widgets = { + 'status': StaticSelect2, + 'type': StaticSelect2, + 'length_unit': StaticSelect2, + } class CableCSVForm(forms.ModelForm): @@ -2983,9 +3740,7 @@ class CableCSVForm(forms.ModelForm): ) side_a_type = forms.ModelChoiceField( queryset=ContentType.objects.all(), - limit_choices_to={ - 'model__in': CABLE_TERMINATION_TYPES, - }, + limit_choices_to=CABLE_TERMINATION_MODELS, to_field_name='model', help_text='Side A type' ) @@ -3004,9 +3759,7 @@ class CableCSVForm(forms.ModelForm): ) side_b_type = forms.ModelChoiceField( queryset=ContentType.objects.all(), - limit_choices_to={ - 'model__in': CABLE_TERMINATION_TYPES, - }, + limit_choices_to=CABLE_TERMINATION_MODELS, to_field_name='model', help_text='Side B type' ) @@ -3016,17 +3769,17 @@ class CableCSVForm(forms.ModelForm): # Cable attributes status = CSVChoiceField( - choices=CONNECTION_STATUS_CHOICES, + choices=CableStatusChoices, required=False, help_text='Connection status' ) type = CSVChoiceField( - choices=CABLE_TYPE_CHOICES, + choices=CableTypeChoices, required=False, help_text='Cable type' ) length_unit = CSVChoiceField( - choices=CABLE_LENGTH_UNIT_CHOICES, + choices=CableLengthUnitChoices, required=False, help_text='Length unit' ) @@ -3106,13 +3859,13 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm): widget=forms.MultipleHiddenInput ) type = forms.ChoiceField( - choices=add_blank_choice(CABLE_TYPE_CHOICES), + choices=add_blank_choice(CableTypeChoices), required=False, initial='', widget=StaticSelect2() ) status = forms.ChoiceField( - choices=add_blank_choice(CONNECTION_STATUS_CHOICES), + choices=add_blank_choice(CableStatusChoices), required=False, widget=StaticSelect2(), initial='' @@ -3122,7 +3875,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm): required=False ) color = forms.CharField( - max_length=6, + max_length=6, # RGB color code required=False, widget=ColorSelect() ) @@ -3131,7 +3884,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm): required=False ) length_unit = forms.ChoiceField( - choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES), + choices=add_blank_choice(CableLengthUnitChoices), required=False, initial='', widget=StaticSelect2() @@ -3159,42 +3912,59 @@ class CableFilterForm(BootstrapMixin, forms.Form): required=False, label='Search' ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", filter_for={ 'rack_id': 'site', + 'device_id': 'site', } ) ) - rack_id = FilterChoiceField( + tenant = DynamicModelMultipleChoiceField( + queryset=Tenant.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/tenancy/tenants/", + value_field='slug', + filter_for={ + 'device_id': 'tenant', + } + ) + ) + rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), + required=False, label='Rack', - null_label='-- None --', widget=APISelectMultiple( api_url="/api/dcim/racks/", null_option=True, + filter_for={ + 'device_id': 'rack_id', + } ) ) type = forms.MultipleChoiceField( - choices=add_blank_choice(CABLE_TYPE_CHOICES), + choices=add_blank_choice(CableTypeChoices), required=False, widget=StaticSelect2() ) status = forms.ChoiceField( required=False, - choices=add_blank_choice(CONNECTION_STATUS_CHOICES), + choices=add_blank_choice(CableStatusChoices), widget=StaticSelect2() ) color = forms.CharField( - max_length=6, + max_length=6, # RGB color code required=False, widget=ColorSelect() ) - device_id = FilterChoiceField( + device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, label='Device', @@ -3208,6 +3978,11 @@ class CableFilterForm(BootstrapMixin, forms.Form): # Device bays # +class DeviceBayFilterForm(DeviceComponentFilterForm): + model = DeviceBay + tag = TagFilterField(model) + + class DeviceBayForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -3223,7 +3998,13 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm): } -class DeviceBayCreateForm(ComponentForm): +class DeviceBayCreateForm(BootstrapMixin, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) name_pattern = ExpandableNameField( label='Name' ) @@ -3249,10 +4030,60 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form): rack=device_bay.device.rack, parent_bay__isnull=True, device_type__u_height=0, - device_type__subdevice_role=SUBDEVICE_ROLE_CHILD + device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD ).exclude(pk=device_bay.device.pk) +class DeviceBayCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + installed_device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Child device not found.', + } + ) + + class Meta: + model = DeviceBay + fields = DeviceBay.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit installed device choices to devices of the correct type and location + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['installed_device'].queryset = Device.objects.filter( + site=device.site, + rack=device.rack, + parent_bay__isnull=True, + device_type__u_height=0, + device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD + ).exclude(pk=device.pk) + else: + self.fields['installed_device'].queryset = Interface.objects.none() + + class DeviceBayBulkRenameForm(BulkRenameForm): pk = forms.ModelMultipleChoiceField( queryset=DeviceBay.objects.all(), @@ -3265,15 +4096,19 @@ class DeviceBayBulkRenameForm(BulkRenameForm): # class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", + filter_for={ + 'device_id': 'site', + } ) ) - device_id = FilterChoiceField( + device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, label='Device', @@ -3284,15 +4119,19 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): class PowerConnectionFilterForm(BootstrapMixin, forms.Form): - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", + filter_for={ + 'device_id': 'site', + } ) ) - device_id = FilterChoiceField( + device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, label='Device', @@ -3303,15 +4142,19 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form): class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", + filter_for={ + 'device_id': 'site', + } ) ) - device_id = FilterChoiceField( + device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, label='Device', @@ -3326,6 +4169,19 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): # class InventoryItemForm(BootstrapMixin, forms.ModelForm): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/" + ) + ) + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/manufacturers/" + ) + ) tags = TagField( required=False ) @@ -3335,14 +4191,42 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm): fields = [ 'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags', ] - widgets = { - 'device': APISelect( - api_url="/api/dcim/devices/" - ), - 'manufacturer': APISelect( - api_url="/api/dcim/manufacturers/" - ) - } + + +class InventoryItemCreateForm(BootstrapMixin, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer'), + widget=APISelect( + api_url="/api/dcim/devices/", + ) + ) + name_pattern = ExpandableNameField( + label='Name' + ) + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/manufacturers/" + ) + ) + part_id = forms.CharField( + max_length=50, + required=False, + label='Part ID' + ) + serial = forms.CharField( + max_length=50, + required=False, + ) + asset_tag = forms.CharField( + max_length=50, + required=False, + ) + description = forms.CharField( + max_length=100, + required=False + ) class InventoryItemCSVForm(forms.ModelForm): @@ -3374,14 +4258,14 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm): queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput() ) - device = forms.ModelChoiceField( + device = DynamicModelChoiceField( queryset=Device.objects.all(), required=False, widget=APISelect( api_url="/api/dcim/devices/" ) ) - manufacturer = forms.ModelChoiceField( + manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), required=False, widget=APISelect( @@ -3410,7 +4294,7 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): required=False, label='Search' ) - region = FilterChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -3422,9 +4306,10 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): } ) ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", @@ -3433,7 +4318,7 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): } ) ) - device_id = FilterChoiceField( + device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, label='Device', @@ -3441,9 +4326,10 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): api_url='/api/dcim/devices/', ) ) - manufacturer = FilterChoiceField( + manufacturer = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), to_field_name='slug', + required=False, widget=APISelect( api_url="/api/dcim/manufacturers/", value_field="slug", @@ -3455,6 +4341,7 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + tag = TagFilterField(model) # @@ -3537,10 +4424,9 @@ class DeviceVCMembershipForm(forms.ModelForm): return vc_position -class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): - site = forms.ModelChoiceField( +class VCMemberSelectForm(BootstrapMixin, forms.Form): + site = DynamicModelChoiceField( queryset=Site.objects.all(), - label='Site', required=False, widget=APISelect( api_url="/api/dcim/sites/", @@ -3550,12 +4436,8 @@ class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): } ) ) - rack = ChainedModelChoiceField( + rack = DynamicModelChoiceField( queryset=Rack.objects.all(), - chains=( - ('site', 'site'), - ), - label='Rack', required=False, widget=APISelect( api_url='/api/dcim/racks/', @@ -3567,15 +4449,10 @@ class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): } ) ) - device = ChainedModelChoiceField( + device = DynamicModelChoiceField( queryset=Device.objects.filter( virtual_chassis__isnull=True ), - chains=( - ('site', 'site'), - ('rack', 'rack'), - ), - label='Device', widget=APISelect( api_url='/api/dcim/devices/', display_field='display_name', @@ -3598,7 +4475,7 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) - region = FilterChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -3610,18 +4487,19 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): } ) ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", ) ) - tenant_group = FilterChoiceField( + tenant_group = DynamicModelMultipleChoiceField( queryset=TenantGroup.objects.all(), to_field_name='slug', - null_label='-- None --', + required=False, widget=APISelectMultiple( api_url="/api/tenancy/tenant-groups/", value_field="slug", @@ -3631,16 +4509,17 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): } ) ) - tenant = FilterChoiceField( + tenant = DynamicModelMultipleChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', - null_label='-- None --', + required=False, widget=APISelectMultiple( api_url="/api/tenancy/tenants/", value_field="slug", null_option=True, ) ) + tag = TagFilterField(model) # @@ -3648,11 +4527,17 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): # class PowerPanelForm(BootstrapMixin, forms.ModelForm): - rack_group = ChainedModelChoiceField( + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + widget=APISelect( + api_url="/api/dcim/sites/", + filter_for={ + 'rack_group': 'site_id', + } + ) + ) + rack_group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), - chains=( - ('site', 'site'), - ), required=False, widget=APISelect( api_url='/api/dcim/rack-groups/', @@ -3664,14 +4549,6 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm): fields = [ 'site', 'rack_group', 'name', ] - widgets = { - 'site': APISelect( - api_url="/api/dcim/sites/", - filter_for={ - 'rack_group': 'site_id', - } - ), - } class PowerPanelCSVForm(forms.ModelForm): @@ -3715,7 +4592,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) - region = FilterChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -3727,9 +4604,10 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): } ) ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", @@ -3738,10 +4616,10 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): } ) ) - rack_group_id = FilterChoiceField( + rack_group_id = DynamicModelMultipleChoiceField( queryset=RackGroup.objects.all(), + required=False, label='Rack group (ID)', - null_label='-- None --', widget=APISelectMultiple( api_url="/api/dcim/rack-groups/", null_option=True, @@ -3753,8 +4631,8 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): # Power feeds # -class PowerFeedForm(BootstrapMixin, CustomFieldForm): - site = ChainedModelChoiceField( +class PowerFeedForm(BootstrapMixin, CustomFieldModelForm): + site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, widget=APISelect( @@ -3765,6 +4643,19 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm): } ) ) + power_panel = DynamicModelChoiceField( + queryset=PowerPanel.objects.all(), + widget=APISelect( + api_url="/api/dcim/power-panels/" + ) + ) + rack = DynamicModelChoiceField( + queryset=Rack.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/racks/" + ) + ) comments = CommentField() tags = TagField( required=False @@ -3777,12 +4668,6 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm): 'max_utilization', 'comments', 'tags', ] widgets = { - 'power_panel': APISelect( - api_url="/api/dcim/power-panels/" - ), - 'rack': APISelect( - api_url="/api/dcim/racks/" - ), 'status': StaticSelect2(), 'type': StaticSelect2(), 'supply': StaticSelect2(), @@ -3798,7 +4683,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm): self.initial['site'] = self.instance.power_panel.site -class PowerFeedCSVForm(forms.ModelForm): +class PowerFeedCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -3824,22 +4709,22 @@ class PowerFeedCSVForm(forms.ModelForm): help_text="Rack name (optional)" ) status = CSVChoiceField( - choices=POWERFEED_STATUS_CHOICES, + choices=PowerFeedStatusChoices, required=False, help_text='Operational status' ) type = CSVChoiceField( - choices=POWERFEED_TYPE_CHOICES, + choices=PowerFeedTypeChoices, required=False, help_text='Primary or redundant' ) supply = CSVChoiceField( - choices=POWERFEED_SUPPLY_CHOICES, + choices=PowerFeedSupplyChoices, required=False, help_text='AC/DC' ) phase = CSVChoiceField( - choices=POWERFEED_PHASE_CHOICES, + choices=PowerFeedPhaseChoices, required=False, help_text='Single or three-phase' ) @@ -3881,7 +4766,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd queryset=PowerFeed.objects.all(), widget=forms.MultipleHiddenInput ) - powerpanel = forms.ModelChoiceField( + power_panel = DynamicModelChoiceField( queryset=PowerPanel.objects.all(), required=False, widget=APISelect( @@ -3891,7 +4776,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd } ) ) - rack = forms.ModelChoiceField( + rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, widget=APISelect( @@ -3899,25 +4784,25 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd ) ) status = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_STATUS_CHOICES), + choices=add_blank_choice(PowerFeedStatusChoices), required=False, initial='', widget=StaticSelect2() ) type = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_TYPE_CHOICES), + choices=add_blank_choice(PowerFeedTypeChoices), required=False, initial='', widget=StaticSelect2() ) supply = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES), + choices=add_blank_choice(PowerFeedSupplyChoices), required=False, initial='', widget=StaticSelect2() ) phase = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_PHASE_CHOICES), + choices=add_blank_choice(PowerFeedPhaseChoices), required=False, initial='', widget=StaticSelect2() @@ -3931,8 +4816,9 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd max_utilization = forms.IntegerField( required=False ) - comments = forms.CharField( - required=False + comments = CommentField( + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -3947,7 +4833,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) - region = FilterChoiceField( + region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, @@ -3959,9 +4845,10 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): } ) ) - site = FilterChoiceField( + site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', + required=False, widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", @@ -3971,41 +4858,41 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): } ) ) - power_panel_id = FilterChoiceField( + power_panel_id = DynamicModelMultipleChoiceField( queryset=PowerPanel.objects.all(), + required=False, label='Power panel', - null_label='-- None --', widget=APISelectMultiple( api_url="/api/dcim/power-panels/", null_option=True, ) ) - rack_id = FilterChoiceField( + rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), + required=False, label='Rack', - null_label='-- None --', widget=APISelectMultiple( api_url="/api/dcim/racks/", null_option=True, ) ) status = forms.MultipleChoiceField( - choices=POWERFEED_STATUS_CHOICES, + choices=PowerFeedStatusChoices, required=False, widget=StaticSelect2Multiple() ) type = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_TYPE_CHOICES), + choices=add_blank_choice(PowerFeedTypeChoices), required=False, widget=StaticSelect2() ) supply = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES), + choices=add_blank_choice(PowerFeedSupplyChoices), required=False, widget=StaticSelect2() ) phase = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_PHASE_CHOICES), + choices=add_blank_choice(PowerFeedPhaseChoices), required=False, widget=StaticSelect2() ) @@ -4018,3 +4905,4 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): max_utilization = forms.IntegerField( required=False ) + tag = TagFilterField(model) diff --git a/netbox/dcim/managers.py b/netbox/dcim/managers.py deleted file mode 100644 index e1124b84e..000000000 --- a/netbox/dcim/managers.py +++ /dev/null @@ -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) diff --git a/netbox/dcim/migrations/0002_auto_20160622_1821_squashed_0022_color_names_to_rgb.py b/netbox/dcim/migrations/0002_auto_20160622_1821_squashed_0022_color_names_to_rgb.py deleted file mode 100644 index c3412cf10..000000000 --- a/netbox/dcim/migrations/0002_auto_20160622_1821_squashed_0022_color_names_to_rgb.py +++ /dev/null @@ -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), - ), - ] diff --git a/netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py b/netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py new file mode 100644 index 000000000..a9f80f49b --- /dev/null +++ b/netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py @@ -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'), + ), + ] diff --git a/netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py b/netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py new file mode 100644 index 000000000..dac983398 --- /dev/null +++ b/netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py @@ -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), + ), + ] diff --git a/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py b/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py index 4d4cfb603..064832e80 100644 --- a/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py +++ b/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py @@ -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.core.validators -from django.db import migrations, models import django.db.models.deletion import mptt.fields +from django.conf import settings +from django.db import migrations, models + +import dcim.fields import utilities.fields @@ -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')] dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('dcim', '0022_color_names_to_rgb'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -94,10 +93,15 @@ class Migration(migrations.Migration): name='site', field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'), ), - migrations.AddField( + migrations.AlterField( 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'), + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), ), migrations.CreateModel( name='Region', @@ -157,7 +161,17 @@ class Migration(migrations.Migration): 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'), + field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200), ), migrations.AlterField( model_name='consoleport', @@ -199,6 +213,11 @@ class Migration(migrations.Migration): name='serial', field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), ), + migrations.AlterField( + model_name='device', + name='status', + field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'), + ), migrations.AlterField( model_name='devicebay', name='name', @@ -244,6 +263,16 @@ class Migration(migrations.Migration): name='u_height', field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'), ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AddField( + model_name='interface', + name='lag', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'), + ), migrations.AlterField( model_name='interface', name='mac_address', @@ -259,6 +288,11 @@ class Migration(migrations.Migration): name='connection_status', field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'), ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), migrations.AlterField( model_name='interfacetemplate', name='mgmt_only', @@ -329,6 +363,16 @@ class Migration(migrations.Migration): name='contact_email', field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'), ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), migrations.AddField( model_name='interface', name='enabled', diff --git a/netbox/dcim/migrations/0044_virtualization_squashed_0055_virtualchassis_ordering.py b/netbox/dcim/migrations/0044_virtualization_squashed_0055_virtualchassis_ordering.py deleted file mode 100644 index 78b4e3a41..000000000 --- a/netbox/dcim/migrations/0044_virtualization_squashed_0055_virtualchassis_ordering.py +++ /dev/null @@ -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), - ), - ] diff --git a/netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py b/netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py new file mode 100644 index 000000000..18ef39fe7 --- /dev/null +++ b/netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py @@ -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'), + ), + ] diff --git a/netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py b/netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py new file mode 100644 index 000000000..71ce4191f --- /dev/null +++ b/netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py @@ -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'), + ), + ] diff --git a/netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py b/netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py new file mode 100644 index 000000000..6fbf115d9 --- /dev/null +++ b/netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py @@ -0,0 +1,146 @@ +import taggit.managers +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('dcim', '0067_device_type_remove_qualifiers'), ('dcim', '0068_rack_new_fields'), ('dcim', '0069_deprecate_nullablecharfield'), ('dcim', '0070_custom_tag_models')] + + dependencies = [ + ('extras', '0019_tag_taggeditem'), + ('dcim', '0066_cables'), + ] + + operations = [ + migrations.RemoveField( + model_name='devicetype', + name='is_console_server', + ), + migrations.RemoveField( + model_name='devicetype', + name='is_network_device', + ), + migrations.RemoveField( + model_name='devicetype', + name='is_pdu', + ), + migrations.RemoveField( + model_name='devicetype', + name='interface_ordering', + ), + migrations.AddField( + model_name='rack', + name='status', + field=models.PositiveSmallIntegerField(default=3), + ), + migrations.AddField( + model_name='rack', + name='outer_depth', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rack', + name='outer_unit', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rack', + name='outer_width', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='device', + name='asset_tag', + field=models.CharField(blank=True, max_length=50, null=True, unique=True), + ), + migrations.AlterField( + model_name='device', + name='name', + field=models.CharField(blank=True, max_length=64, null=True, unique=True), + ), + migrations.AlterField( + model_name='inventoryitem', + name='asset_tag', + field=models.CharField(blank=True, max_length=50, null=True, unique=True), + ), + migrations.AddField( + model_name='rack', + name='asset_tag', + field=models.CharField(blank=True, max_length=50, null=True, unique=True), + ), + migrations.AlterField( + model_name='rack', + name='facility_id', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AlterField( + model_name='consoleport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='consoleserverport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='device', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='devicebay', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='devicetype', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='frontport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='interface', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='poweroutlet', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='powerport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='rack', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='rearport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='site', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='virtualchassis', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + ] diff --git a/netbox/dcim/migrations/0071_device_components_add_description_squashed_0088_powerfeed_available_power.py b/netbox/dcim/migrations/0071_device_components_add_description_squashed_0088_powerfeed_available_power.py new file mode 100644 index 000000000..f74572c6f --- /dev/null +++ b/netbox/dcim/migrations/0071_device_components_add_description_squashed_0088_powerfeed_available_power.py @@ -0,0 +1,839 @@ +import sys + +import django.core.validators +import django.db.models.deletion +import taggit.managers +from django.db import migrations, models + +SITE_STATUS_CHOICES = ( + (1, 'active'), + (2, 'planned'), + (4, 'retired'), +) + +RACK_TYPE_CHOICES = ( + (100, '2-post-frame'), + (200, '4-post-frame'), + (300, '4-post-cabinet'), + (1000, 'wall-frame'), + (1100, 'wall-cabinet'), +) + +RACK_STATUS_CHOICES = ( + (0, 'reserved'), + (1, 'available'), + (2, 'planned'), + (3, 'active'), + (4, 'deprecated'), +) + +RACK_DIMENSION_CHOICES = ( + (1000, 'mm'), + (2000, 'in'), +) + +SUBDEVICE_ROLE_CHOICES = ( + ('true', 'parent'), + ('false', 'child'), +) + +DEVICE_FACE_CHOICES = ( + (0, 'front'), + (1, 'rear'), +) + +DEVICE_STATUS_CHOICES = ( + (0, 'offline'), + (1, 'active'), + (2, 'planned'), + (3, 'staged'), + (4, 'failed'), + (5, 'inventory'), + (6, 'decommissioning'), +) + +INTERFACE_TYPE_CHOICES = ( + (0, 'virtual'), + (200, 'lag'), + (800, '100base-tx'), + (1000, '1000base-t'), + (1050, '1000base-x-gbic'), + (1100, '1000base-x-sfp'), + (1120, '2.5gbase-t'), + (1130, '5gbase-t'), + (1150, '10gbase-t'), + (1170, '10gbase-cx4'), + (1200, '10gbase-x-sfpp'), + (1300, '10gbase-x-xfp'), + (1310, '10gbase-x-xenpak'), + (1320, '10gbase-x-x2'), + (1350, '25gbase-x-sfp28'), + (1400, '40gbase-x-qsfpp'), + (1420, '50gbase-x-sfp28'), + (1500, '100gbase-x-cfp'), + (1510, '100gbase-x-cfp2'), + (1520, '100gbase-x-cfp4'), + (1550, '100gbase-x-cpak'), + (1600, '100gbase-x-qsfp28'), + (1650, '200gbase-x-cfp2'), + (1700, '200gbase-x-qsfp56'), + (1750, '400gbase-x-qsfpdd'), + (1800, '400gbase-x-osfp'), + (2600, 'ieee802.11a'), + (2610, 'ieee802.11g'), + (2620, 'ieee802.11n'), + (2630, 'ieee802.11ac'), + (2640, 'ieee802.11ad'), + (2810, 'gsm'), + (2820, 'cdma'), + (2830, 'lte'), + (6100, 'sonet-oc3'), + (6200, 'sonet-oc12'), + (6300, 'sonet-oc48'), + (6400, 'sonet-oc192'), + (6500, 'sonet-oc768'), + (6600, 'sonet-oc1920'), + (6700, 'sonet-oc3840'), + (3010, '1gfc-sfp'), + (3020, '2gfc-sfp'), + (3040, '4gfc-sfp'), + (3080, '8gfc-sfpp'), + (3160, '16gfc-sfpp'), + (3320, '32gfc-sfp28'), + (3400, '128gfc-sfp28'), + (7010, 'inifiband-sdr'), + (7020, 'inifiband-ddr'), + (7030, 'inifiband-qdr'), + (7040, 'inifiband-fdr10'), + (7050, 'inifiband-fdr'), + (7060, 'inifiband-edr'), + (7070, 'inifiband-hdr'), + (7080, 'inifiband-ndr'), + (7090, 'inifiband-xdr'), + (4000, 't1'), + (4010, 'e1'), + (4040, 't3'), + (4050, 'e3'), + (5000, 'cisco-stackwise'), + (5050, 'cisco-stackwise-plus'), + (5100, 'cisco-flexstack'), + (5150, 'cisco-flexstack-plus'), + (5200, 'juniper-vcp'), + (5300, 'extreme-summitstack'), + (5310, 'extreme-summitstack-128'), + (5320, 'extreme-summitstack-256'), + (5330, 'extreme-summitstack-512'), +) + +INTERFACE_MODE_CHOICES = ( + (100, 'access'), + (200, 'tagged'), + (300, 'tagged-all'), +) + +PORT_TYPE_CHOICES = ( + (1000, '8p8c'), + (1100, '110-punch'), + (1200, 'bnc'), + (2000, 'st'), + (2100, 'sc'), + (2110, 'sc-apc'), + (2200, 'fc'), + (2300, 'lc'), + (2310, 'lc-apc'), + (2400, 'mtrj'), + (2500, 'mpo'), + (2600, 'lsh'), + (2610, 'lsh-apc'), +) + +CABLE_TYPE_CHOICES = ( + (1300, 'cat3'), + (1500, 'cat5'), + (1510, 'cat5e'), + (1600, 'cat6'), + (1610, 'cat6a'), + (1700, 'cat7'), + (1800, 'dac-active'), + (1810, 'dac-passive'), + (1900, 'coaxial'), + (3000, 'mmf'), + (3010, 'mmf-om1'), + (3020, 'mmf-om2'), + (3030, 'mmf-om3'), + (3040, 'mmf-om4'), + (3500, 'smf'), + (3510, 'smf-os1'), + (3520, 'smf-os2'), + (3800, 'aoc'), + (5000, 'power'), +) + +CABLE_STATUS_CHOICES = ( + ('true', 'connected'), + ('false', 'planned'), +) + +CABLE_LENGTH_UNIT_CHOICES = ( + (1200, 'm'), + (1100, 'cm'), + (2100, 'ft'), + (2000, 'in'), +) + +POWERFEED_STATUS_CHOICES = ( + (0, 'offline'), + (1, 'active'), + (2, 'planned'), + (4, 'failed'), +) + +POWERFEED_TYPE_CHOICES = ( + (1, 'primary'), + (2, 'redundant'), +) + +POWERFEED_SUPPLY_CHOICES = ( + (1, 'ac'), + (2, 'dc'), +) + +POWERFEED_PHASE_CHOICES = ( + (1, 'single-phase'), + (3, 'three-phase'), +) + +POWEROUTLET_FEED_LEG_CHOICES_CHOICES = ( + (1, 'A'), + (2, 'B'), + (3, 'C'), +) + + +def cache_cable_devices(apps, schema_editor): + Cable = apps.get_model('dcim', 'Cable') + + if 'test' not in sys.argv: + print("\nUpdating cable device terminations...") + cable_count = Cable.objects.count() + + # Cache A/B termination devices on all existing Cables. Note that the custom save() method on Cable is not + # available during a migration, so we replicate its logic here. + for i, cable in enumerate(Cable.objects.all(), start=1): + + if not i % 1000 and 'test' not in sys.argv: + print("[{}/{}]".format(i, cable_count)) + + termination_a_model = apps.get_model(cable.termination_a_type.app_label, cable.termination_a_type.model) + termination_a_device = None + if hasattr(termination_a_model, 'device'): + termination_a = termination_a_model.objects.get(pk=cable.termination_a_id) + termination_a_device = termination_a.device + + termination_b_model = apps.get_model(cable.termination_b_type.app_label, cable.termination_b_type.model) + termination_b_device = None + if hasattr(termination_b_model, 'device'): + termination_b = termination_b_model.objects.get(pk=cable.termination_b_id) + termination_b_device = termination_b.device + + Cable.objects.filter(pk=cable.pk).update( + _termination_a_device=termination_a_device, + _termination_b_device=termination_b_device + ) + + +def site_status_to_slug(apps, schema_editor): + Site = apps.get_model('dcim', 'Site') + for id, slug in SITE_STATUS_CHOICES: + Site.objects.filter(status=str(id)).update(status=slug) + + +def rack_type_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_TYPE_CHOICES: + Rack.objects.filter(type=str(id)).update(type=slug) + + +def rack_status_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_STATUS_CHOICES: + Rack.objects.filter(status=str(id)).update(status=slug) + + +def rack_outer_unit_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_DIMENSION_CHOICES: + Rack.objects.filter(status=str(id)).update(status=slug) + + +def devicetype_subdevicerole_to_slug(apps, schema_editor): + DeviceType = apps.get_model('dcim', 'DeviceType') + for boolean, slug in SUBDEVICE_ROLE_CHOICES: + DeviceType.objects.filter(subdevice_role=boolean).update(subdevice_role=slug) + + +def device_face_to_slug(apps, schema_editor): + Device = apps.get_model('dcim', 'Device') + for id, slug in DEVICE_FACE_CHOICES: + Device.objects.filter(face=str(id)).update(face=slug) + + +def device_status_to_slug(apps, schema_editor): + Device = apps.get_model('dcim', 'Device') + for id, slug in DEVICE_STATUS_CHOICES: + Device.objects.filter(status=str(id)).update(status=slug) + + +def interfacetemplate_type_to_slug(apps, schema_editor): + InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate') + for id, slug in INTERFACE_TYPE_CHOICES: + InterfaceTemplate.objects.filter(type=id).update(type=slug) + + +def interface_type_to_slug(apps, schema_editor): + Interface = apps.get_model('dcim', 'Interface') + for id, slug in INTERFACE_TYPE_CHOICES: + Interface.objects.filter(type=id).update(type=slug) + + +def interface_mode_to_slug(apps, schema_editor): + Interface = apps.get_model('dcim', 'Interface') + for id, slug in INTERFACE_MODE_CHOICES: + Interface.objects.filter(mode=id).update(mode=slug) + + +def frontporttemplate_type_to_slug(apps, schema_editor): + FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate') + for id, slug in PORT_TYPE_CHOICES: + FrontPortTemplate.objects.filter(type=id).update(type=slug) + + +def rearporttemplate_type_to_slug(apps, schema_editor): + RearPortTemplate = apps.get_model('dcim', 'RearPortTemplate') + for id, slug in PORT_TYPE_CHOICES: + RearPortTemplate.objects.filter(type=id).update(type=slug) + + +def frontport_type_to_slug(apps, schema_editor): + FrontPort = apps.get_model('dcim', 'FrontPort') + for id, slug in PORT_TYPE_CHOICES: + FrontPort.objects.filter(type=id).update(type=slug) + + +def rearport_type_to_slug(apps, schema_editor): + RearPort = apps.get_model('dcim', 'RearPort') + for id, slug in PORT_TYPE_CHOICES: + RearPort.objects.filter(type=id).update(type=slug) + + +def cable_type_to_slug(apps, schema_editor): + Cable = apps.get_model('dcim', 'Cable') + for id, slug in CABLE_TYPE_CHOICES: + Cable.objects.filter(type=id).update(type=slug) + + +def cable_status_to_slug(apps, schema_editor): + Cable = apps.get_model('dcim', 'Cable') + for bool_str, slug in CABLE_STATUS_CHOICES: + Cable.objects.filter(status=bool_str).update(status=slug) + + +def cable_length_unit_to_slug(apps, schema_editor): + Cable = apps.get_model('dcim', 'Cable') + for id, slug in CABLE_LENGTH_UNIT_CHOICES: + Cable.objects.filter(length_unit=id).update(length_unit=slug) + + +def powerfeed_status_to_slug(apps, schema_editor): + PowerFeed = apps.get_model('dcim', 'PowerFeed') + for id, slug in POWERFEED_STATUS_CHOICES: + PowerFeed.objects.filter(status=id).update(status=slug) + + +def powerfeed_type_to_slug(apps, schema_editor): + PowerFeed = apps.get_model('dcim', 'PowerFeed') + for id, slug in POWERFEED_TYPE_CHOICES: + PowerFeed.objects.filter(type=id).update(type=slug) + + +def powerfeed_supply_to_slug(apps, schema_editor): + PowerFeed = apps.get_model('dcim', 'PowerFeed') + for id, slug in POWERFEED_SUPPLY_CHOICES: + PowerFeed.objects.filter(supply=id).update(supply=slug) + + +def powerfeed_phase_to_slug(apps, schema_editor): + PowerFeed = apps.get_model('dcim', 'PowerFeed') + for id, slug in POWERFEED_PHASE_CHOICES: + PowerFeed.objects.filter(phase=id).update(phase=slug) + + +def poweroutlettemplate_feed_leg_to_slug(apps, schema_editor): + PowerOutletTemplate = apps.get_model('dcim', 'PowerOutletTemplate') + for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES: + PowerOutletTemplate.objects.filter(feed_leg=id).update(feed_leg=slug) + + +def poweroutlet_feed_leg_to_slug(apps, schema_editor): + PowerOutlet = apps.get_model('dcim', 'PowerOutlet') + for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES: + PowerOutlet.objects.filter(feed_leg=id).update(feed_leg=slug) + + +class Migration(migrations.Migration): + + replaces = [('dcim', '0071_device_components_add_description'), ('dcim', '0072_powerfeeds'), ('dcim', '0073_interface_form_factor_to_type'), ('dcim', '0074_increase_field_length_platform_name_slug'), ('dcim', '0075_cable_devices'), ('dcim', '0076_console_port_types'), ('dcim', '0077_power_types'), ('dcim', '0078_3569_site_fields'), ('dcim', '0079_3569_rack_fields'), ('dcim', '0080_3569_devicetype_fields'), ('dcim', '0081_3569_device_fields'), ('dcim', '0082_3569_interface_fields'), ('dcim', '0082_3569_port_fields'), ('dcim', '0083_3569_cable_fields'), ('dcim', '0084_3569_powerfeed_fields'), ('dcim', '0085_3569_poweroutlet_fields'), ('dcim', '0086_device_name_nonunique'), ('dcim', '0087_role_descriptions'), ('dcim', '0088_powerfeed_available_power')] + + dependencies = [ + ('dcim', '0070_custom_tag_models'), + ('extras', '0021_add_color_comments_changelog_to_tag'), + ('tenancy', '0006_custom_tag_models'), + ] + + operations = [ + migrations.AddField( + model_name='consoleport', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='consoleserverport', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='devicebay', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='poweroutlet', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='powerport', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.CreateModel( + name='PowerPanel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=50)), + ('rack_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.RackGroup')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dcim.Site')), + ], + options={ + 'ordering': ['site', 'name'], + 'unique_together': {('site', 'name')}, + }, + ), + migrations.CreateModel( + name='PowerFeed', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=50)), + ('status', models.PositiveSmallIntegerField(default=1)), + ('type', models.PositiveSmallIntegerField(default=1)), + ('supply', models.PositiveSmallIntegerField(default=1)), + ('phase', models.PositiveSmallIntegerField(default=1)), + ('voltage', models.PositiveSmallIntegerField(default=120, validators=[django.core.validators.MinValueValidator(1)])), + ('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])), + ('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])), + ('available_power', models.PositiveSmallIntegerField(default=0, editable=False)), + ('comments', models.TextField(blank=True)), + ('cable', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable')), + ('power_panel', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.PowerPanel')), + ('rack', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.Rack')), + ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags')), + ('connected_endpoint', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerPort')), + ('connection_status', models.NullBooleanField()), + ], + options={ + 'ordering': ['power_panel', 'name'], + 'unique_together': {('power_panel', 'name')}, + }, + ), + migrations.RenameField( + model_name='powerport', + old_name='connected_endpoint', + new_name='_connected_poweroutlet', + ), + migrations.AddField( + model_name='powerport', + name='_connected_powerfeed', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerFeed'), + ), + migrations.AddField( + model_name='powerport', + name='allocated_draw', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='powerport', + name='maximum_draw', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='powerporttemplate', + name='allocated_draw', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='powerporttemplate', + name='maximum_draw', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='poweroutlet', + name='feed_leg', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='poweroutlet', + name='power_port', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlets', to='dcim.PowerPort'), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='feed_leg', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='power_port', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlet_templates', to='dcim.PowerPortTemplate'), + ), + migrations.RenameField( + model_name='interface', + old_name='form_factor', + new_name='type', + ), + migrations.RenameField( + model_name='interfacetemplate', + old_name='form_factor', + new_name='type', + ), + migrations.AlterField( + model_name='platform', + name='name', + field=models.CharField(max_length=100, unique=True), + ), + migrations.AlterField( + model_name='platform', + name='slug', + field=models.SlugField(max_length=100, unique=True), + ), + migrations.AddField( + model_name='cable', + name='_termination_a_device', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.Device'), + ), + migrations.AddField( + model_name='cable', + name='_termination_b_device', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.Device'), + ), + migrations.RunPython( + code=cache_cable_devices, + reverse_code=django.db.migrations.operations.special.RunPython.noop, + ), + migrations.AddField( + model_name='consoleport', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='consoleporttemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='consoleserverport', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='consoleserverporttemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='poweroutlet', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='powerport', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='powerporttemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='site', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=site_status_to_slug, + ), + migrations.AlterField( + model_name='rack', + name='type', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=rack_type_to_slug, + ), + migrations.AlterField( + model_name='rack', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='rack', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=rack_status_to_slug, + ), + migrations.AlterField( + model_name='rack', + name='outer_unit', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=rack_outer_unit_to_slug, + ), + migrations.AlterField( + model_name='rack', + name='outer_unit', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='devicetype', + name='subdevice_role', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=devicetype_subdevicerole_to_slug, + ), + migrations.AlterField( + model_name='devicetype', + name='subdevice_role', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='device', + name='face', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=device_face_to_slug, + ), + migrations.AlterField( + model_name='device', + name='face', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='device', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=device_status_to_slug, + ), + migrations.AlterField( + model_name='interfacetemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=interfacetemplate_type_to_slug, + ), + migrations.AlterField( + model_name='interface', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=interface_type_to_slug, + ), + migrations.AlterField( + model_name='interface', + name='mode', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=interface_mode_to_slug, + ), + migrations.AlterField( + model_name='interface', + name='mode', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='frontporttemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=frontporttemplate_type_to_slug, + ), + migrations.AlterField( + model_name='rearporttemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=rearporttemplate_type_to_slug, + ), + migrations.AlterField( + model_name='frontport', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=frontport_type_to_slug, + ), + migrations.AlterField( + model_name='rearport', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=rearport_type_to_slug, + ), + migrations.AlterField( + model_name='cable', + name='type', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=cable_type_to_slug, + ), + migrations.AlterField( + model_name='cable', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='cable', + name='status', + field=models.CharField(default='connected', max_length=50), + ), + migrations.RunPython( + code=cable_status_to_slug, + ), + migrations.AlterField( + model_name='cable', + name='length_unit', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=cable_length_unit_to_slug, + ), + migrations.AlterField( + model_name='cable', + name='length_unit', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='powerfeed', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=powerfeed_status_to_slug, + ), + migrations.AlterField( + model_name='powerfeed', + name='type', + field=models.CharField(default='primary', max_length=50), + ), + migrations.RunPython( + code=powerfeed_type_to_slug, + ), + migrations.AlterField( + model_name='powerfeed', + name='supply', + field=models.CharField(default='ac', max_length=50), + ), + migrations.RunPython( + code=powerfeed_supply_to_slug, + ), + migrations.AlterField( + model_name='powerfeed', + name='phase', + field=models.CharField(default='single-phase', max_length=50), + ), + migrations.RunPython( + code=powerfeed_phase_to_slug, + ), + migrations.AlterField( + model_name='poweroutlettemplate', + name='feed_leg', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=poweroutlettemplate_feed_leg_to_slug, + ), + migrations.AlterField( + model_name='poweroutlettemplate', + name='feed_leg', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='poweroutlet', + name='feed_leg', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=poweroutlet_feed_leg_to_slug, + ), + migrations.AlterField( + model_name='poweroutlet', + name='feed_leg', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='device', + name='name', + field=models.CharField(blank=True, max_length=64, null=True), + ), + migrations.AlterUniqueTogether( + name='device', + unique_together={('rack', 'position', 'face'), ('site', 'tenant', 'name'), ('virtual_chassis', 'vc_position')}, + ), + migrations.AddField( + model_name='devicerole', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='rackrole', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AlterField( + model_name='powerfeed', + name='available_power', + field=models.PositiveIntegerField(default=0, editable=False), + ), + ] diff --git a/netbox/dcim/migrations/0076_console_port_types.py b/netbox/dcim/migrations/0076_console_port_types.py new file mode 100644 index 000000000..844b32283 --- /dev/null +++ b/netbox/dcim/migrations/0076_console_port_types.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.6 on 2019-10-30 17:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0075_cable_devices'), + ] + + operations = [ + migrations.AddField( + model_name='consoleport', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='consoleporttemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='consoleserverport', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='consoleserverporttemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/dcim/migrations/0077_power_types.py b/netbox/dcim/migrations/0077_power_types.py new file mode 100644 index 000000000..702bd837b --- /dev/null +++ b/netbox/dcim/migrations/0077_power_types.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.6 on 2019-11-06 19:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0076_console_port_types'), + ] + + operations = [ + migrations.AddField( + model_name='poweroutlet', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='powerport', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='powerporttemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/dcim/migrations/0078_3569_site_fields.py b/netbox/dcim/migrations/0078_3569_site_fields.py new file mode 100644 index 000000000..8775abe5e --- /dev/null +++ b/netbox/dcim/migrations/0078_3569_site_fields.py @@ -0,0 +1,35 @@ +from django.db import migrations, models + +SITE_STATUS_CHOICES = ( + (1, 'active'), + (2, 'planned'), + (4, 'retired'), +) + + +def site_status_to_slug(apps, schema_editor): + Site = apps.get_model('dcim', 'Site') + for id, slug in SITE_STATUS_CHOICES: + Site.objects.filter(status=str(id)).update(status=slug) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0077_power_types'), + ] + + operations = [ + + # Site.status + migrations.AlterField( + model_name='site', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=site_status_to_slug + ), + + ] diff --git a/netbox/dcim/migrations/0079_3569_rack_fields.py b/netbox/dcim/migrations/0079_3569_rack_fields.py new file mode 100644 index 000000000..da544bb7a --- /dev/null +++ b/netbox/dcim/migrations/0079_3569_rack_fields.py @@ -0,0 +1,92 @@ +from django.db import migrations, models + +RACK_TYPE_CHOICES = ( + (100, '2-post-frame'), + (200, '4-post-frame'), + (300, '4-post-cabinet'), + (1000, 'wall-frame'), + (1100, 'wall-cabinet'), +) + +RACK_STATUS_CHOICES = ( + (0, 'reserved'), + (1, 'available'), + (2, 'planned'), + (3, 'active'), + (4, 'deprecated'), +) + +RACK_DIMENSION_CHOICES = ( + (1000, 'mm'), + (2000, 'in'), +) + + +def rack_type_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_TYPE_CHOICES: + Rack.objects.filter(type=str(id)).update(type=slug) + + +def rack_status_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_STATUS_CHOICES: + Rack.objects.filter(status=str(id)).update(status=slug) + + +def rack_outer_unit_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_DIMENSION_CHOICES: + Rack.objects.filter(outer_unit=str(id)).update(outer_unit=slug) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0078_3569_site_fields'), + ] + + operations = [ + + # Rack.type + migrations.AlterField( + model_name='rack', + name='type', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=rack_type_to_slug + ), + migrations.AlterField( + model_name='rack', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + + # Rack.status + migrations.AlterField( + model_name='rack', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=rack_status_to_slug + ), + + # Rack.outer_unit + migrations.AlterField( + model_name='rack', + name='outer_unit', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=rack_outer_unit_to_slug + ), + migrations.AlterField( + model_name='rack', + name='outer_unit', + field=models.CharField(blank=True, max_length=50), + ), + + ] diff --git a/netbox/dcim/migrations/0080_3569_devicetype_fields.py b/netbox/dcim/migrations/0080_3569_devicetype_fields.py new file mode 100644 index 000000000..e729eaa55 --- /dev/null +++ b/netbox/dcim/migrations/0080_3569_devicetype_fields.py @@ -0,0 +1,39 @@ +from django.db import migrations, models + +SUBDEVICE_ROLE_CHOICES = ( + ('true', 'parent'), + ('false', 'child'), +) + + +def devicetype_subdevicerole_to_slug(apps, schema_editor): + DeviceType = apps.get_model('dcim', 'DeviceType') + for boolean, slug in SUBDEVICE_ROLE_CHOICES: + DeviceType.objects.filter(subdevice_role=boolean).update(subdevice_role=slug) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0079_3569_rack_fields'), + ] + + operations = [ + + # DeviceType.subdevice_role + migrations.AlterField( + model_name='devicetype', + name='subdevice_role', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=devicetype_subdevicerole_to_slug + ), + migrations.AlterField( + model_name='devicetype', + name='subdevice_role', + field=models.CharField(blank=True, max_length=50), + ), + + ] diff --git a/netbox/dcim/migrations/0081_3569_device_fields.py b/netbox/dcim/migrations/0081_3569_device_fields.py new file mode 100644 index 000000000..f1f0bdb2b --- /dev/null +++ b/netbox/dcim/migrations/0081_3569_device_fields.py @@ -0,0 +1,65 @@ +from django.db import migrations, models + +DEVICE_FACE_CHOICES = ( + (0, 'front'), + (1, 'rear'), +) + +DEVICE_STATUS_CHOICES = ( + (0, 'offline'), + (1, 'active'), + (2, 'planned'), + (3, 'staged'), + (4, 'failed'), + (5, 'inventory'), + (6, 'decommissioning'), +) + + +def device_face_to_slug(apps, schema_editor): + Device = apps.get_model('dcim', 'Device') + for id, slug in DEVICE_FACE_CHOICES: + Device.objects.filter(face=str(id)).update(face=slug) + + +def device_status_to_slug(apps, schema_editor): + Device = apps.get_model('dcim', 'Device') + for id, slug in DEVICE_STATUS_CHOICES: + Device.objects.filter(status=str(id)).update(status=slug) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0080_3569_devicetype_fields'), + ] + + operations = [ + + # Device.face + migrations.AlterField( + model_name='device', + name='face', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=device_face_to_slug + ), + migrations.AlterField( + model_name='device', + name='face', + field=models.CharField(blank=True, max_length=50), + ), + + # Device.status + migrations.AlterField( + model_name='device', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=device_status_to_slug + ), + + ] diff --git a/netbox/dcim/migrations/0082_3569_interface_fields.py b/netbox/dcim/migrations/0082_3569_interface_fields.py new file mode 100644 index 000000000..57701ce0a --- /dev/null +++ b/netbox/dcim/migrations/0082_3569_interface_fields.py @@ -0,0 +1,147 @@ +from django.db import migrations, models + + +INTERFACE_TYPE_CHOICES = ( + (0, 'virtual'), + (200, 'lag'), + (800, '100base-tx'), + (1000, '1000base-t'), + (1050, '1000base-x-gbic'), + (1100, '1000base-x-sfp'), + (1120, '2.5gbase-t'), + (1130, '5gbase-t'), + (1150, '10gbase-t'), + (1170, '10gbase-cx4'), + (1200, '10gbase-x-sfpp'), + (1300, '10gbase-x-xfp'), + (1310, '10gbase-x-xenpak'), + (1320, '10gbase-x-x2'), + (1350, '25gbase-x-sfp28'), + (1400, '40gbase-x-qsfpp'), + (1420, '50gbase-x-sfp28'), + (1500, '100gbase-x-cfp'), + (1510, '100gbase-x-cfp2'), + (1520, '100gbase-x-cfp4'), + (1550, '100gbase-x-cpak'), + (1600, '100gbase-x-qsfp28'), + (1650, '200gbase-x-cfp2'), + (1700, '200gbase-x-qsfp56'), + (1750, '400gbase-x-qsfpdd'), + (1800, '400gbase-x-osfp'), + (2600, 'ieee802.11a'), + (2610, 'ieee802.11g'), + (2620, 'ieee802.11n'), + (2630, 'ieee802.11ac'), + (2640, 'ieee802.11ad'), + (2810, 'gsm'), + (2820, 'cdma'), + (2830, 'lte'), + (6100, 'sonet-oc3'), + (6200, 'sonet-oc12'), + (6300, 'sonet-oc48'), + (6400, 'sonet-oc192'), + (6500, 'sonet-oc768'), + (6600, 'sonet-oc1920'), + (6700, 'sonet-oc3840'), + (3010, '1gfc-sfp'), + (3020, '2gfc-sfp'), + (3040, '4gfc-sfp'), + (3080, '8gfc-sfpp'), + (3160, '16gfc-sfpp'), + (3320, '32gfc-sfp28'), + (3400, '128gfc-sfp28'), + (7010, 'inifiband-sdr'), + (7020, 'inifiband-ddr'), + (7030, 'inifiband-qdr'), + (7040, 'inifiband-fdr10'), + (7050, 'inifiband-fdr'), + (7060, 'inifiband-edr'), + (7070, 'inifiband-hdr'), + (7080, 'inifiband-ndr'), + (7090, 'inifiband-xdr'), + (4000, 't1'), + (4010, 'e1'), + (4040, 't3'), + (4050, 'e3'), + (5000, 'cisco-stackwise'), + (5050, 'cisco-stackwise-plus'), + (5100, 'cisco-flexstack'), + (5150, 'cisco-flexstack-plus'), + (5200, 'juniper-vcp'), + (5300, 'extreme-summitstack'), + (5310, 'extreme-summitstack-128'), + (5320, 'extreme-summitstack-256'), + (5330, 'extreme-summitstack-512'), +) + + +INTERFACE_MODE_CHOICES = ( + (100, 'access'), + (200, 'tagged'), + (300, 'tagged-all'), +) + + +def interfacetemplate_type_to_slug(apps, schema_editor): + InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate') + for id, slug in INTERFACE_TYPE_CHOICES: + InterfaceTemplate.objects.filter(type=id).update(type=slug) + + +def interface_type_to_slug(apps, schema_editor): + Interface = apps.get_model('dcim', 'Interface') + for id, slug in INTERFACE_TYPE_CHOICES: + Interface.objects.filter(type=id).update(type=slug) + + +def interface_mode_to_slug(apps, schema_editor): + Interface = apps.get_model('dcim', 'Interface') + for id, slug in INTERFACE_MODE_CHOICES: + Interface.objects.filter(mode=id).update(mode=slug) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0081_3569_device_fields'), + ] + + operations = [ + + # InterfaceTemplate.type + migrations.AlterField( + model_name='interfacetemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=interfacetemplate_type_to_slug + ), + + # Interface.type + migrations.AlterField( + model_name='interface', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=interface_type_to_slug + ), + + # Interface.mode + migrations.AlterField( + model_name='interface', + name='mode', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=interface_mode_to_slug + ), + migrations.AlterField( + model_name='interface', + name='mode', + field=models.CharField(blank=True, max_length=50), + ), + + ] diff --git a/netbox/dcim/migrations/0082_3569_port_fields.py b/netbox/dcim/migrations/0082_3569_port_fields.py new file mode 100644 index 000000000..6d8f50c32 --- /dev/null +++ b/netbox/dcim/migrations/0082_3569_port_fields.py @@ -0,0 +1,93 @@ +from django.db import migrations, models + + +PORT_TYPE_CHOICES = ( + (1000, '8p8c'), + (1100, '110-punch'), + (1200, 'bnc'), + (2000, 'st'), + (2100, 'sc'), + (2110, 'sc-apc'), + (2200, 'fc'), + (2300, 'lc'), + (2310, 'lc-apc'), + (2400, 'mtrj'), + (2500, 'mpo'), + (2600, 'lsh'), + (2610, 'lsh-apc'), +) + + +def frontporttemplate_type_to_slug(apps, schema_editor): + FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate') + for id, slug in PORT_TYPE_CHOICES: + FrontPortTemplate.objects.filter(type=id).update(type=slug) + + +def rearporttemplate_type_to_slug(apps, schema_editor): + RearPortTemplate = apps.get_model('dcim', 'RearPortTemplate') + for id, slug in PORT_TYPE_CHOICES: + RearPortTemplate.objects.filter(type=id).update(type=slug) + + +def frontport_type_to_slug(apps, schema_editor): + FrontPort = apps.get_model('dcim', 'FrontPort') + for id, slug in PORT_TYPE_CHOICES: + FrontPort.objects.filter(type=id).update(type=slug) + + +def rearport_type_to_slug(apps, schema_editor): + RearPort = apps.get_model('dcim', 'RearPort') + for id, slug in PORT_TYPE_CHOICES: + RearPort.objects.filter(type=id).update(type=slug) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0082_3569_interface_fields'), + ] + + operations = [ + + # FrontPortTemplate.type + migrations.AlterField( + model_name='frontporttemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=frontporttemplate_type_to_slug + ), + + # RearPortTemplate.type + migrations.AlterField( + model_name='rearporttemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=rearporttemplate_type_to_slug + ), + + # FrontPort.type + migrations.AlterField( + model_name='frontport', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=frontport_type_to_slug + ), + + # RearPort.type + migrations.AlterField( + model_name='rearport', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=rearport_type_to_slug + ), + ] diff --git a/netbox/dcim/migrations/0083_3569_cable_fields.py b/netbox/dcim/migrations/0083_3569_cable_fields.py new file mode 100644 index 000000000..26cf734f7 --- /dev/null +++ b/netbox/dcim/migrations/0083_3569_cable_fields.py @@ -0,0 +1,106 @@ +from django.db import migrations, models + + +CABLE_TYPE_CHOICES = ( + (1300, 'cat3'), + (1500, 'cat5'), + (1510, 'cat5e'), + (1600, 'cat6'), + (1610, 'cat6a'), + (1700, 'cat7'), + (1800, 'dac-active'), + (1810, 'dac-passive'), + (1900, 'coaxial'), + (3000, 'mmf'), + (3010, 'mmf-om1'), + (3020, 'mmf-om2'), + (3030, 'mmf-om3'), + (3040, 'mmf-om4'), + (3500, 'smf'), + (3510, 'smf-os1'), + (3520, 'smf-os2'), + (3800, 'aoc'), + (5000, 'power'), +) + +CABLE_STATUS_CHOICES = ( + ('true', 'connected'), + ('false', 'planned'), +) + +CABLE_LENGTH_UNIT_CHOICES = ( + (1200, 'm'), + (1100, 'cm'), + (2100, 'ft'), + (2000, 'in'), +) + + +def cable_type_to_slug(apps, schema_editor): + Cable = apps.get_model('dcim', 'Cable') + for id, slug in CABLE_TYPE_CHOICES: + Cable.objects.filter(type=id).update(type=slug) + + +def cable_status_to_slug(apps, schema_editor): + Cable = apps.get_model('dcim', 'Cable') + for bool_str, slug in CABLE_STATUS_CHOICES: + Cable.objects.filter(status=bool_str).update(status=slug) + + +def cable_length_unit_to_slug(apps, schema_editor): + Cable = apps.get_model('dcim', 'Cable') + for id, slug in CABLE_LENGTH_UNIT_CHOICES: + Cable.objects.filter(length_unit=id).update(length_unit=slug) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0082_3569_port_fields'), + ] + + operations = [ + + # Cable.type + migrations.AlterField( + model_name='cable', + name='type', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=cable_type_to_slug + ), + migrations.AlterField( + model_name='cable', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + + # Cable.status + migrations.AlterField( + model_name='cable', + name='status', + field=models.CharField(default='connected', max_length=50), + ), + migrations.RunPython( + code=cable_status_to_slug + ), + + # Cable.length_unit + migrations.AlterField( + model_name='cable', + name='length_unit', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=cable_length_unit_to_slug + ), + migrations.AlterField( + model_name='cable', + name='length_unit', + field=models.CharField(blank=True, max_length=50), + ), + + ] diff --git a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py new file mode 100644 index 000000000..332443d0a --- /dev/null +++ b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py @@ -0,0 +1,100 @@ +from django.db import migrations, models + + +POWERFEED_STATUS_CHOICES = ( + (0, 'offline'), + (1, 'active'), + (2, 'planned'), + (4, 'failed'), +) + +POWERFEED_TYPE_CHOICES = ( + (1, 'primary'), + (2, 'redundant'), +) + +POWERFEED_SUPPLY_CHOICES = ( + (1, 'ac'), + (2, 'dc'), +) + +POWERFEED_PHASE_CHOICES = ( + (1, 'single-phase'), + (3, 'three-phase'), +) + + +def powerfeed_status_to_slug(apps, schema_editor): + PowerFeed = apps.get_model('dcim', 'PowerFeed') + for id, slug in POWERFEED_STATUS_CHOICES: + PowerFeed.objects.filter(status=id).update(status=slug) + + +def powerfeed_type_to_slug(apps, schema_editor): + PowerFeed = apps.get_model('dcim', 'PowerFeed') + for id, slug in POWERFEED_TYPE_CHOICES: + PowerFeed.objects.filter(type=id).update(type=slug) + + +def powerfeed_supply_to_slug(apps, schema_editor): + PowerFeed = apps.get_model('dcim', 'PowerFeed') + for id, slug in POWERFEED_SUPPLY_CHOICES: + PowerFeed.objects.filter(supply=id).update(supply=slug) + + +def powerfeed_phase_to_slug(apps, schema_editor): + PowerFeed = apps.get_model('dcim', 'PowerFeed') + for id, slug in POWERFEED_PHASE_CHOICES: + PowerFeed.objects.filter(phase=id).update(phase=slug) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0083_3569_cable_fields'), + ] + + operations = [ + + # PowerFeed.status + migrations.AlterField( + model_name='powerfeed', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=powerfeed_status_to_slug + ), + + # PowerFeed.type + migrations.AlterField( + model_name='powerfeed', + name='type', + field=models.CharField(default='primary', max_length=50), + ), + migrations.RunPython( + code=powerfeed_type_to_slug + ), + + # PowerFeed.supply + migrations.AlterField( + model_name='powerfeed', + name='supply', + field=models.CharField(default='ac', max_length=50), + ), + migrations.RunPython( + code=powerfeed_supply_to_slug + ), + + # PowerFeed.phase + migrations.AlterField( + model_name='powerfeed', + name='phase', + field=models.CharField(default='single-phase', max_length=50), + ), + migrations.RunPython( + code=powerfeed_phase_to_slug + ), + + ] diff --git a/netbox/dcim/migrations/0085_3569_poweroutlet_fields.py b/netbox/dcim/migrations/0085_3569_poweroutlet_fields.py new file mode 100644 index 000000000..e2c070584 --- /dev/null +++ b/netbox/dcim/migrations/0085_3569_poweroutlet_fields.py @@ -0,0 +1,62 @@ +from django.db import migrations, models + + +POWEROUTLET_FEED_LEG_CHOICES_CHOICES = ( + (1, 'A'), + (2, 'B'), + (3, 'C'), +) + + +def poweroutlettemplate_feed_leg_to_slug(apps, schema_editor): + PowerOutletTemplate = apps.get_model('dcim', 'PowerOutletTemplate') + for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES: + PowerOutletTemplate.objects.filter(feed_leg=id).update(feed_leg=slug) + + +def poweroutlet_feed_leg_to_slug(apps, schema_editor): + PowerOutlet = apps.get_model('dcim', 'PowerOutlet') + for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES: + PowerOutlet.objects.filter(feed_leg=id).update(feed_leg=slug) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('dcim', '0084_3569_powerfeed_fields'), + ] + + operations = [ + + # PowerOutletTemplate.feed_leg + migrations.AlterField( + model_name='poweroutlettemplate', + name='feed_leg', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=poweroutlettemplate_feed_leg_to_slug + ), + migrations.AlterField( + model_name='poweroutlettemplate', + name='feed_leg', + field=models.CharField(blank=True, max_length=50), + ), + + # PowerOutlet.feed_leg + migrations.AlterField( + model_name='poweroutlet', + name='feed_leg', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.RunPython( + code=poweroutlet_feed_leg_to_slug + ), + migrations.AlterField( + model_name='poweroutlet', + name='feed_leg', + field=models.CharField(blank=True, max_length=50), + ), + + ] diff --git a/netbox/dcim/migrations/0086_device_name_nonunique.py b/netbox/dcim/migrations/0086_device_name_nonunique.py new file mode 100644 index 000000000..3666cf018 --- /dev/null +++ b/netbox/dcim/migrations/0086_device_name_nonunique.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.6 on 2019-12-09 15:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0006_custom_tag_models'), + ('dcim', '0085_3569_poweroutlet_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='device', + name='name', + field=models.CharField(blank=True, max_length=64, null=True), + ), + migrations.AlterUniqueTogether( + name='device', + unique_together={('rack', 'position', 'face'), ('virtual_chassis', 'vc_position'), ('site', 'tenant', 'name')}, + ), + ] diff --git a/netbox/dcim/migrations/0087_role_descriptions.py b/netbox/dcim/migrations/0087_role_descriptions.py new file mode 100644 index 000000000..5f8fd9707 --- /dev/null +++ b/netbox/dcim/migrations/0087_role_descriptions.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.6 on 2019-12-10 17:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0086_device_name_nonunique'), + ] + + operations = [ + migrations.AddField( + model_name='devicerole', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='rackrole', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/netbox/dcim/migrations/0088_powerfeed_available_power.py b/netbox/dcim/migrations/0088_powerfeed_available_power.py new file mode 100644 index 000000000..af13d49c6 --- /dev/null +++ b/netbox/dcim/migrations/0088_powerfeed_available_power.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.8 on 2019-12-12 02:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0087_role_descriptions'), + ] + + operations = [ + migrations.AlterField( + model_name='powerfeed', + name='available_power', + field=models.PositiveIntegerField(default=0, editable=False), + ), + ] diff --git a/netbox/dcim/migrations/0089_deterministic_ordering.py b/netbox/dcim/migrations/0089_deterministic_ordering.py new file mode 100644 index 000000000..6944cff00 --- /dev/null +++ b/netbox/dcim/migrations/0089_deterministic_ordering.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.8 on 2020-01-15 18:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0088_powerfeed_available_power'), + ] + + operations = [ + migrations.AlterModelOptions( + name='device', + options={'ordering': ('name', 'pk'), 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))}, + ), + migrations.AlterModelOptions( + name='rack', + options={'ordering': ('site', 'group', 'name', 'pk')}, + ), + ] diff --git a/netbox/dcim/migrations/0090_cable_termination_models.py b/netbox/dcim/migrations/0090_cable_termination_models.py new file mode 100644 index 000000000..b5f240f3e --- /dev/null +++ b/netbox/dcim/migrations/0090_cable_termination_models.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.8 on 2020-01-15 20:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0089_deterministic_ordering'), + ] + + operations = [ + migrations.AlterField( + model_name='cable', + name='termination_a_type', + field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'), + ), + migrations.AlterField( + model_name='cable', + name='termination_b_type', + field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'), + ), + ] diff --git a/netbox/dcim/migrations/0091_interface_type_other.py b/netbox/dcim/migrations/0091_interface_type_other.py new file mode 100644 index 000000000..1ea24885f --- /dev/null +++ b/netbox/dcim/migrations/0091_interface_type_other.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +def interface_type_to_slug(apps, schema_editor): + Interface = apps.get_model('dcim', 'Interface') + Interface.objects.filter(type=32767).update(type='other') + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0090_cable_termination_models'), + ] + + operations = [ + # Missed type "other" in the initial migration (see #3967) + migrations.RunPython( + code=interface_type_to_slug + ), + ] diff --git a/netbox/dcim/migrations/0092_fix_rack_outer_unit.py b/netbox/dcim/migrations/0092_fix_rack_outer_unit.py new file mode 100644 index 000000000..2a8cbf4e5 --- /dev/null +++ b/netbox/dcim/migrations/0092_fix_rack_outer_unit.py @@ -0,0 +1,27 @@ +from django.db import migrations + +RACK_DIMENSION_CHOICES = ( + (1000, 'mm'), + (2000, 'in'), +) + + +def rack_outer_unit_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_DIMENSION_CHOICES: + Rack.objects.filter(outer_unit=str(id)).update(outer_unit=slug) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0091_interface_type_other'), + ] + + operations = [ + # Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed, + # so this can be omitted when squashing in the future. + migrations.RunPython( + code=rack_outer_unit_to_slug + ), + ] diff --git a/netbox/dcim/migrations/0093_device_component_ordering.py b/netbox/dcim/migrations/0093_device_component_ordering.py new file mode 100644 index 000000000..4e3c941a1 --- /dev/null +++ b/netbox/dcim/migrations/0093_device_component_ordering.py @@ -0,0 +1,147 @@ +from django.db import migrations +import utilities.fields +import utilities.ordering + + +def _update_model_names(model): + # Update each unique field value in bulk + for name in model.objects.values_list('name', flat=True).order_by('name').distinct(): + model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100)) + + +def naturalize_consoleports(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'ConsolePort')) + + +def naturalize_consoleserverports(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'ConsoleServerPort')) + + +def naturalize_powerports(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'PowerPort')) + + +def naturalize_poweroutlets(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'PowerOutlet')) + + +def naturalize_frontports(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'FrontPort')) + + +def naturalize_rearports(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'RearPort')) + + +def naturalize_devicebays(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'DeviceBay')) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0092_fix_rack_outer_unit'), + ] + + operations = [ + migrations.AlterModelOptions( + name='consoleport', + options={'ordering': ('device', '_name')}, + ), + migrations.AlterModelOptions( + name='consoleserverport', + options={'ordering': ('device', '_name')}, + ), + migrations.AlterModelOptions( + name='devicebay', + options={'ordering': ('device', '_name')}, + ), + migrations.AlterModelOptions( + name='frontport', + options={'ordering': ('device', '_name')}, + ), + migrations.AlterModelOptions( + name='inventoryitem', + options={'ordering': ('device__id', 'parent__id', '_name')}, + ), + migrations.AlterModelOptions( + name='poweroutlet', + options={'ordering': ('device', '_name')}, + ), + migrations.AlterModelOptions( + name='powerport', + options={'ordering': ('device', '_name')}, + ), + migrations.AlterModelOptions( + name='rearport', + options={'ordering': ('device', '_name')}, + ), + migrations.AddField( + model_name='consoleport', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='consoleserverport', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='devicebay', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='frontport', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='inventoryitem', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='poweroutlet', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='powerport', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='rearport', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.RunPython( + code=naturalize_consoleports, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_consoleserverports, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_powerports, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_poweroutlets, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_frontports, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_rearports, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_devicebays, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/migrations/0094_device_component_template_ordering.py b/netbox/dcim/migrations/0094_device_component_template_ordering.py new file mode 100644 index 000000000..24fe98e94 --- /dev/null +++ b/netbox/dcim/migrations/0094_device_component_template_ordering.py @@ -0,0 +1,138 @@ +from django.db import migrations +import utilities.fields +import utilities.ordering + + +def _update_model_names(model): + # Update each unique field value in bulk + for name in model.objects.values_list('name', flat=True).order_by('name').distinct(): + model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100)) + + +def naturalize_consoleporttemplates(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'ConsolePortTemplate')) + + +def naturalize_consoleserverporttemplates(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'ConsoleServerPortTemplate')) + + +def naturalize_powerporttemplates(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'PowerPortTemplate')) + + +def naturalize_poweroutlettemplates(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'PowerOutletTemplate')) + + +def naturalize_frontporttemplates(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'FrontPortTemplate')) + + +def naturalize_rearporttemplates(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'RearPortTemplate')) + + +def naturalize_devicebaytemplates(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'DeviceBayTemplate')) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0093_device_component_ordering'), + ] + + operations = [ + migrations.AlterModelOptions( + name='consoleporttemplate', + options={'ordering': ('device_type', '_name')}, + ), + migrations.AlterModelOptions( + name='consoleserverporttemplate', + options={'ordering': ('device_type', '_name')}, + ), + migrations.AlterModelOptions( + name='devicebaytemplate', + options={'ordering': ('device_type', '_name')}, + ), + migrations.AlterModelOptions( + name='frontporttemplate', + options={'ordering': ('device_type', '_name')}, + ), + migrations.AlterModelOptions( + name='poweroutlettemplate', + options={'ordering': ('device_type', '_name')}, + ), + migrations.AlterModelOptions( + name='powerporttemplate', + options={'ordering': ('device_type', '_name')}, + ), + migrations.AlterModelOptions( + name='rearporttemplate', + options={'ordering': ('device_type', '_name')}, + ), + migrations.AddField( + model_name='consoleporttemplate', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='consoleserverporttemplate', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='devicebaytemplate', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='frontporttemplate', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='powerporttemplate', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='rearporttemplate', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.RunPython( + code=naturalize_consoleporttemplates, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_consoleserverporttemplates, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_powerporttemplates, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_poweroutlettemplates, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_frontporttemplates, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_rearporttemplates, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_devicebaytemplates, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/migrations/0095_primary_model_ordering.py b/netbox/dcim/migrations/0095_primary_model_ordering.py new file mode 100644 index 000000000..3bc780161 --- /dev/null +++ b/netbox/dcim/migrations/0095_primary_model_ordering.py @@ -0,0 +1,70 @@ +from django.db import migrations +import utilities.fields +import utilities.ordering + + +def _update_model_names(model): + # Update each unique field value in bulk + for name in model.objects.values_list('name', flat=True).order_by('name').distinct(): + model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100)) + + +def naturalize_sites(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'Site')) + + +def naturalize_racks(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'Rack')) + + +def naturalize_devices(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'Device')) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0094_device_component_template_ordering'), + ] + + operations = [ + migrations.AlterModelOptions( + name='device', + options={'ordering': ('_name', 'pk'), 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))}, + ), + migrations.AlterModelOptions( + name='rack', + options={'ordering': ('site', 'group', '_name', 'pk')}, + ), + migrations.AlterModelOptions( + name='site', + options={'ordering': ('_name',)}, + ), + migrations.AddField( + model_name='device', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize, null=True), + ), + migrations.AddField( + model_name='rack', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.AddField( + model_name='site', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize), + ), + migrations.RunPython( + code=naturalize_sites, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_racks, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_devices, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/migrations/0096_interface_ordering.py b/netbox/dcim/migrations/0096_interface_ordering.py new file mode 100644 index 000000000..f1622f504 --- /dev/null +++ b/netbox/dcim/migrations/0096_interface_ordering.py @@ -0,0 +1,53 @@ +from django.db import migrations +import utilities.fields +import utilities.ordering + + +def _update_model_names(model): + # Update each unique field value in bulk + for name in model.objects.values_list('name', flat=True).order_by('name').distinct(): + model.objects.filter(name=name).update(_name=utilities.ordering.naturalize_interface(name, max_length=100)) + + +def naturalize_interfacetemplates(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'InterfaceTemplate')) + + +def naturalize_interfaces(apps, schema_editor): + _update_model_names(apps.get_model('dcim', 'Interface')) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0095_primary_model_ordering'), + ] + + operations = [ + migrations.AlterModelOptions( + name='interface', + options={'ordering': ('device', '_name')}, + ), + migrations.AlterModelOptions( + name='interfacetemplate', + options={'ordering': ('device_type', '_name')}, + ), + migrations.AddField( + model_name='interface', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface), + ), + migrations.AddField( + model_name='interfacetemplate', + name='_name', + field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface), + ), + migrations.RunPython( + code=naturalize_interfacetemplates, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=naturalize_interfaces, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/migrations/0097_interfacetemplate_type_other.py b/netbox/dcim/migrations/0097_interfacetemplate_type_other.py new file mode 100644 index 000000000..d71b5c655 --- /dev/null +++ b/netbox/dcim/migrations/0097_interfacetemplate_type_other.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +def interfacetemplate_type_to_slug(apps, schema_editor): + InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate') + InterfaceTemplate.objects.filter(type=32767).update(type='other') + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0096_interface_ordering'), + ] + + operations = [ + # Missed type "other" in the initial migration (see #3967) + migrations.RunPython( + code=interfacetemplate_type_to_slug + ), + ] diff --git a/netbox/dcim/migrations/0098_devicetype_images.py b/netbox/dcim/migrations/0098_devicetype_images.py new file mode 100644 index 000000000..837a2b73a --- /dev/null +++ b/netbox/dcim/migrations/0098_devicetype_images.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.9 on 2020-02-20 15:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0097_interfacetemplate_type_other'), + ] + + operations = [ + migrations.AddField( + model_name='devicetype', + name='front_image', + field=models.ImageField(blank=True, upload_to='devicetype-images'), + ), + migrations.AddField( + model_name='devicetype', + name='rear_image', + field=models.ImageField(blank=True, upload_to='devicetype-images'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models/__init__.py similarity index 56% rename from netbox/dcim/models.py rename to netbox/dcim/models/__init__.py index 9999f89dc..716959c6b 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models/__init__.py @@ -1,6 +1,7 @@ from collections import OrderedDict from itertools import count, groupby +import yaml from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation @@ -9,176 +10,64 @@ from django.contrib.postgres.fields import ArrayField, JSONField from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models import Count, F, ProtectedError, Q, Sum +from django.db.models import Count, F, ProtectedError, Sum from django.urls import reverse from mptt.models import MPTTModel, TreeForeignKey from taggit.managers import TaggableManager from timezone_field import TimeZoneField +from dcim.choices import * +from dcim.constants import * +from dcim.fields import ASNField +from dcim.elevations import RackElevationSVG from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem -from utilities.fields import ColorField -from utilities.managers import NaturalOrderingManager +from utilities.fields import ColorField, NaturalOrderingField from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object, to_meters -from .constants import * -from .exceptions import LoopDetected -from .fields import ASNField, MACAddressField -from .managers import InterfaceManager +from .device_component_templates import ( + ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate, + PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, +) +from .device_components import ( + CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet, + PowerPort, RearPort, +) - -class ComponentTemplateModel(models.Model): - - class Meta: - abstract = True - - def instantiate(self, device): - """ - Instantiate a new component on the specified Device. - """ - raise NotImplementedError() - - def to_objectchange(self, action): - return ObjectChange( - changed_object=self, - object_repr=str(self), - action=action, - related_object=self.device_type, - object_data=serialize_object(self) - ) - - -class ComponentModel(models.Model): - description = models.CharField( - max_length=100, - blank=True - ) - - class Meta: - abstract = True - - def to_objectchange(self, action): - # Annotate the parent Device/VM - try: - parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None) - except ObjectDoesNotExist: - # The parent device/VM has already been deleted - parent = None - - return ObjectChange( - changed_object=self, - object_repr=str(self), - action=action, - related_object=parent, - object_data=serialize_object(self) - ) - - @property - def parent(self): - return getattr(self, 'device', None) - - -class CableTermination(models.Model): - cable = models.ForeignKey( - to='dcim.Cable', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - - # Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted. - _cabled_as_a = GenericRelation( - to='dcim.Cable', - content_type_field='termination_a_type', - object_id_field='termination_a_id' - ) - _cabled_as_b = GenericRelation( - to='dcim.Cable', - content_type_field='termination_b_type', - object_id_field='termination_b_id' - ) - - is_path_endpoint = True - - class Meta: - abstract = True - - def trace(self, position=1, follow_circuits=False, cable_history=None): - """ - Return a list representing a complete cable path, with each individual segment represented as a three-tuple: - [ - (termination A, cable, termination B), - (termination C, cable, termination D), - (termination E, cable, termination F) - ] - """ - def get_peer_port(termination, position=1, follow_circuits=False): - from circuits.models import CircuitTermination - - # Map a front port to its corresponding rear port - if isinstance(termination, FrontPort): - return termination.rear_port, termination.rear_port_position - - # Map a rear port/position to its corresponding front port - elif isinstance(termination, RearPort): - if position not in range(1, termination.positions + 1): - raise Exception("Invalid position for {} ({} positions): {})".format( - termination, termination.positions, position - )) - try: - peer_port = FrontPort.objects.get( - rear_port=termination, - rear_port_position=position, - ) - return peer_port, 1 - except ObjectDoesNotExist: - return None, None - - # Follow a circuit to its other termination - elif isinstance(termination, CircuitTermination) and follow_circuits: - peer_termination = termination.get_peer_termination() - if peer_termination is None: - return None, None - return peer_termination, position - - # Termination is not a pass-through port - else: - return None, None - - if not self.cable: - return [(self, None, None)] - - # Record cable history to detect loops - if cable_history is None: - cable_history = [] - elif self.cable in cable_history: - raise LoopDetected() - cable_history.append(self.cable) - - far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a - path = [(self, self.cable, far_end)] - - peer_port, position = get_peer_port(far_end, position, follow_circuits) - if peer_port is None: - return path - - try: - next_segment = peer_port.trace(position, follow_circuits, cable_history) - except LoopDetected: - return path - - if next_segment is None: - return path + [(peer_port, None, None)] - - return path + next_segment - - def get_cable_peer(self): - if self.cable is None: - return None - if self._cabled_as_a.exists(): - return self.cable.termination_b - if self._cabled_as_b.exists(): - return self.cable.termination_a +__all__ = ( + 'Cable', + 'CableTermination', + 'ConsolePort', + 'ConsolePortTemplate', + 'ConsoleServerPort', + 'ConsoleServerPortTemplate', + 'Device', + 'DeviceBay', + 'DeviceBayTemplate', + 'DeviceRole', + 'DeviceType', + 'FrontPort', + 'FrontPortTemplate', + 'Interface', + 'InterfaceTemplate', + 'InventoryItem', + 'Manufacturer', + 'Platform', + 'PowerFeed', + 'PowerOutlet', + 'PowerOutletTemplate', + 'PowerPanel', + 'PowerPort', + 'PowerPortTemplate', + 'Rack', + 'RackGroup', + 'RackReservation', + 'RackRole', + 'RearPort', + 'RearPortTemplate', + 'Region', + 'Site', + 'VirtualChassis', +) # @@ -234,6 +123,15 @@ class Region(MPTTModel, ChangeLoggedModel, CustomFieldModel): Q(region__in=self.get_descendants()) ).count() + def to_objectchange(self, action): + # Remove MPTT-internal fields + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id']) + ) + # # Sites @@ -248,12 +146,18 @@ class Site(ChangeLoggedModel, CustomFieldModel): max_length=50, unique=True ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) slug = models.SlugField( unique=True ) - status = models.PositiveSmallIntegerField( - choices=SITE_STATUS_CHOICES, - default=SITE_STATUS_ACTIVE + status = models.CharField( + max_length=50, + choices=SiteStatusChoices, + default=SiteStatusChoices.STATUS_ACTIVE ) region = models.ForeignKey( to='dcim.Region', @@ -328,17 +232,25 @@ class Site(ChangeLoggedModel, CustomFieldModel): images = GenericRelation( to='extras.ImageAttachment' ) - - objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = [ 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments', ] + clone_fields = [ + 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', + 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', + ] + + STATUS_CLASS_MAP = { + SiteStatusChoices.STATUS_ACTIVE: 'success', + SiteStatusChoices.STATUS_PLANNED: 'info', + SiteStatusChoices.STATUS_RETIRED: 'danger', + } class Meta: - ordering = ['name'] + ordering = ('_name',) def __str__(self): return self.name @@ -368,7 +280,7 @@ class Site(ChangeLoggedModel, CustomFieldModel): ) def get_status_class(self): - return STATUS_CLASSES[self.status] + return self.STATUS_CLASS_MAP.get(self.status) # @@ -426,8 +338,12 @@ class RackRole(ChangeLoggedModel): unique=True ) color = ColorField() + description = models.CharField( + max_length=100, + blank=True, + ) - csv_headers = ['name', 'slug', 'color'] + csv_headers = ['name', 'slug', 'color', 'description'] class Meta: ordering = ['name'] @@ -443,6 +359,7 @@ class RackRole(ChangeLoggedModel): self.name, self.slug, self.color, + self.description, ) @@ -454,6 +371,11 @@ class Rack(ChangeLoggedModel, CustomFieldModel): name = models.CharField( max_length=50 ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) facility_id = models.CharField( max_length=50, blank=True, @@ -479,9 +401,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel): blank=True, null=True ) - status = models.PositiveSmallIntegerField( - choices=RACK_STATUS_CHOICES, - default=RACK_STATUS_ACTIVE + status = models.CharField( + max_length=50, + choices=RackStatusChoices, + default=RackStatusChoices.STATUS_ACTIVE ) role = models.ForeignKey( to='dcim.RackRole', @@ -503,20 +426,20 @@ class Rack(ChangeLoggedModel, CustomFieldModel): verbose_name='Asset tag', help_text='A unique tag used to identify this rack' ) - type = models.PositiveSmallIntegerField( - choices=RACK_TYPE_CHOICES, + type = models.CharField( + choices=RackTypeChoices, + max_length=50, blank=True, - null=True, verbose_name='Type' ) width = models.PositiveSmallIntegerField( - choices=RACK_WIDTH_CHOICES, - default=RACK_WIDTH_19IN, + choices=RackWidthChoices, + default=RackWidthChoices.WIDTH_19IN, verbose_name='Width', help_text='Rail-to-rail width' ) u_height = models.PositiveSmallIntegerField( - default=42, + default=RACK_U_HEIGHT_DEFAULT, verbose_name='Height (U)', validators=[MinValueValidator(1), MaxValueValidator(100)] ) @@ -533,10 +456,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel): blank=True, null=True ) - outer_unit = models.PositiveSmallIntegerField( - choices=RACK_DIMENSION_UNIT_CHOICES, + outer_unit = models.CharField( + max_length=50, + choices=RackDimensionUnitChoices, blank=True, - null=True ) comments = models.TextField( blank=True @@ -549,21 +472,32 @@ class Rack(ChangeLoggedModel, CustomFieldModel): images = GenericRelation( to='extras.ImageAttachment' ) - - objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = [ 'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', ] + clone_fields = [ + 'site', 'group', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width', + 'outer_depth', 'outer_unit', + ] + + STATUS_CLASS_MAP = { + RackStatusChoices.STATUS_RESERVED: 'warning', + RackStatusChoices.STATUS_AVAILABLE: 'success', + RackStatusChoices.STATUS_PLANNED: 'info', + RackStatusChoices.STATUS_ACTIVE: 'primary', + RackStatusChoices.STATUS_DEPRECATED: 'danger', + } class Meta: - ordering = ['site', 'group', 'name'] - unique_together = [ - ['group', 'name'], - ['group', 'facility_id'], - ] + ordering = ('site', 'group', '_name', 'pk') # (site, group, name) may be non-unique + unique_together = ( + # Name and facility_id must be unique *only* within a RackGroup + ('group', 'name'), + ('group', 'facility_id'), + ) def __str__(self): return self.display_name or super().__str__() @@ -574,10 +508,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel): def clean(self): # Validate outer dimensions and unit - if (self.outer_width is not None or self.outer_depth is not None) and self.outer_unit is None: + if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit: raise ValidationError("Must specify a unit when setting an outer width/depth") elif self.outer_width is None and self.outer_depth is None: - self.outer_unit = None + self.outer_unit = '' if self.pk: # Validate that Rack is tall enough to house the installed Devices @@ -650,16 +584,18 @@ class Rack(ChangeLoggedModel, CustomFieldModel): return "" def get_status_class(self): - return STATUS_CLASSES[self.status] + return self.STATUS_CLASS_MAP.get(self.status) - def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): + def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack - :param remove_redundant: If True, rack units occupied by a device already listed will be omitted + :param expand_devices: When True, all units that a device occupies will be listed with each containing a + reference to the device. When False, only the bottom most unit for a device is included and that unit + contains a height attribute for the device """ elevation = OrderedDict() @@ -668,27 +604,32 @@ class Rack(ChangeLoggedModel, CustomFieldModel): # Add devices to rack units list if self.pk: - for device in Device.objects.prefetch_related('device_type__manufacturer', 'device_role')\ - .annotate(devicebay_count=Count('device_bays'))\ - .exclude(pk=exclude)\ - .filter(rack=self, position__gt=0)\ - .filter(Q(face=face) | Q(device_type__is_full_depth=True)): - if remove_redundant: - elevation[device.position]['device'] = device - for u in range(device.position + 1, device.position + device.device_type.u_height): - elevation.pop(u, None) - else: + queryset = Device.objects.prefetch_related( + 'device_type', + 'device_type__manufacturer', + 'device_role' + ).annotate( + devicebay_count=Count('device_bays') + ).exclude( + pk=exclude + ).filter( + rack=self, + position__gt=0 + ).filter( + Q(face=face) | Q(device_type__is_full_depth=True) + ) + for device in queryset: + if expand_devices: for u in range(device.position, device.position + device.device_type.u_height): elevation[u]['device'] = device + else: + elevation[device.position]['device'] = device + elevation[device.position]['height'] = device.device_type.u_height + for u in range(device.position + 1, device.position + device.device_type.u_height): + elevation.pop(u, None) return [u for u in elevation.values()] - def get_front_elevation(self): - return self.get_rack_units(face=RACK_FACE_FRONT, remove_redundant=True) - - def get_rear_elevation(self): - return self.get_rack_units(face=RACK_FACE_REAR, remove_redundant=True) - def get_available_units(self, u_height=1, rack_face=None, exclude=list()): """ Return a list of units within the rack available to accommodate a device of a given U height (default 1). @@ -734,6 +675,28 @@ class Rack(ChangeLoggedModel, CustomFieldModel): reserved_units[u] = r return reserved_units + def get_elevation_svg( + self, + face=DeviceFaceChoices.FACE_FRONT, + unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT, + unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT, + legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT, + include_images=True + ): + """ + Return an SVG of the rack elevation + + :param face: Enum of [front, rear] representing the desired side of the rack elevation to render + :param unit_width: Width in pixels for the rendered drawing + :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total + height of the elevation + :param legend_width: Width of the unit legend, in pixels + :param include_images: Embed front/rear device images where available + """ + elevation = RackElevationSVG(self, include_images=include_images) + + return elevation.render(face, unit_width, unit_height, legend_width) + def get_0u_devices(self): return self.devices.filter(position=0) @@ -916,12 +879,21 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): verbose_name='Is full depth', help_text='Device consumes both front and rear rack faces' ) - subdevice_role = models.NullBooleanField( - default=None, + subdevice_role = models.CharField( + max_length=50, + choices=SubdeviceRoleChoices, + blank=True, verbose_name='Parent/child status', - choices=SUBDEVICE_ROLE_CHOICES, - help_text='Parent devices house child devices in device bays. Select ' - '"None" if this device type is neither a parent nor a child.' + help_text='Parent devices house child devices in device bays. Leave blank ' + 'if this device type is neither a parent nor a child.' + ) + front_image = models.ImageField( + upload_to='devicetype-images', + blank=True + ) + rear_image = models.ImageField( + upload_to='devicetype-images', + blank=True ) comments = models.TextField( blank=True @@ -934,8 +906,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) - csv_headers = [ - 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments', + clone_fields = [ + 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', ] class Meta: @@ -954,20 +926,99 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): # Save a copy of u_height for validation in clean() self._original_u_height = self.u_height + # Save references to the original front/rear images + self._original_front_image = self.front_image + self._original_rear_image = self.rear_image + def get_absolute_url(self): return reverse('dcim:devicetype', args=[self.pk]) - def to_csv(self): - return ( - self.manufacturer.name, - self.model, - self.slug, - self.part_number, - self.u_height, - self.is_full_depth, - self.get_subdevice_role_display() if self.subdevice_role else None, - self.comments, - ) + def to_yaml(self): + data = OrderedDict(( + ('manufacturer', self.manufacturer.name), + ('model', self.model), + ('slug', self.slug), + ('part_number', self.part_number), + ('u_height', self.u_height), + ('is_full_depth', self.is_full_depth), + ('subdevice_role', self.subdevice_role), + ('comments', self.comments), + )) + + # Component templates + if self.consoleport_templates.exists(): + data['console-ports'] = [ + { + 'name': c.name, + 'type': c.type, + } + for c in self.consoleport_templates.all() + ] + if self.consoleserverport_templates.exists(): + data['console-server-ports'] = [ + { + 'name': c.name, + 'type': c.type, + } + for c in self.consoleserverport_templates.all() + ] + if self.powerport_templates.exists(): + data['power-ports'] = [ + { + 'name': c.name, + 'type': c.type, + 'maximum_draw': c.maximum_draw, + 'allocated_draw': c.allocated_draw, + } + for c in self.powerport_templates.all() + ] + if self.poweroutlet_templates.exists(): + data['power-outlets'] = [ + { + 'name': c.name, + 'type': c.type, + 'power_port': c.power_port.name if c.power_port else None, + 'feed_leg': c.feed_leg, + } + for c in self.poweroutlet_templates.all() + ] + if self.interface_templates.exists(): + data['interfaces'] = [ + { + 'name': c.name, + 'type': c.type, + 'mgmt_only': c.mgmt_only, + } + for c in self.interface_templates.all() + ] + if self.frontport_templates.exists(): + data['front-ports'] = [ + { + 'name': c.name, + 'type': c.type, + 'rear_port': c.rear_port.name, + 'rear_port_position': c.rear_port_position, + } + for c in self.frontport_templates.all() + ] + if self.rearport_templates.exists(): + data['rear-ports'] = [ + { + 'name': c.name, + 'type': c.type, + 'positions': c.positions, + } + for c in self.rearport_templates.all() + ] + if self.device_bay_templates.exists(): + data['device-bays'] = [ + { + 'name': c.name, + } + for c in self.device_bay_templates.all() + ] + + return yaml.dump(dict(data), sort_keys=False) def clean(self): @@ -985,374 +1036,50 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): "{}U".format(d, d.rack, self.u_height) }) - if self.subdevice_role != SUBDEVICE_ROLE_PARENT and self.device_bay_templates.count(): + if ( + self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT + ) and self.device_bay_templates.count(): raise ValidationError({ 'subdevice_role': "Must delete all device bay templates associated with this device before " "declassifying it as a parent device." }) - if self.u_height and self.subdevice_role == SUBDEVICE_ROLE_CHILD: + if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD: raise ValidationError({ 'u_height': "Child device types must be 0U." }) + def save(self, *args, **kwargs): + ret = super().save(*args, **kwargs) + + # Delete any previously uploaded image files that are no longer in use + if self.front_image != self._original_front_image: + self._original_front_image.delete(save=False) + if self.rear_image != self._original_rear_image: + self._original_rear_image.delete(save=False) + + return ret + + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) + + # Delete any uploaded image files + if self.front_image: + self.front_image.delete(save=False) + if self.rear_image: + self.rear_image.delete(save=False) + @property def display_name(self): return '{} {}'.format(self.manufacturer.name, self.model) @property def is_parent_device(self): - return bool(self.subdevice_role) + return self.subdevice_role == SubdeviceRoleChoices.ROLE_PARENT @property def is_child_device(self): - return bool(self.subdevice_role is False) - - -class ConsolePortTemplate(ComponentTemplateModel): - """ - A template for a ConsolePort to be created for a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='consoleport_templates' - ) - name = models.CharField( - max_length=50 - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return ConsolePort( - device=device, - name=self.name - ) - - -class ConsoleServerPortTemplate(ComponentTemplateModel): - """ - A template for a ConsoleServerPort to be created for a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='consoleserverport_templates' - ) - name = models.CharField( - max_length=50 - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return ConsoleServerPort( - device=device, - name=self.name - ) - - -class PowerPortTemplate(ComponentTemplateModel): - """ - A template for a PowerPort to be created for a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='powerport_templates' - ) - name = models.CharField( - max_length=50 - ) - maximum_draw = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1)], - help_text="Maximum current draw (watts)" - ) - allocated_draw = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1)], - help_text="Allocated current draw (watts)" - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return PowerPort( - device=device, - name=self.name, - maximum_draw=self.maximum_draw, - allocated_draw=self.allocated_draw - ) - - -class PowerOutletTemplate(ComponentTemplateModel): - """ - A template for a PowerOutlet to be created for a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='poweroutlet_templates' - ) - name = models.CharField( - max_length=50 - ) - power_port = models.ForeignKey( - to='dcim.PowerPortTemplate', - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name='poweroutlet_templates' - ) - feed_leg = models.PositiveSmallIntegerField( - choices=POWERFEED_LEG_CHOICES, - blank=True, - null=True, - help_text="Phase (for three-phase feeds)" - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def clean(self): - - # Validate power port assignment - if self.power_port and self.power_port.device_type != self.device_type: - raise ValidationError( - "Parent power port ({}) must belong to the same device type".format(self.power_port) - ) - - def instantiate(self, device): - if self.power_port: - power_port = PowerPort.objects.get(device=device, name=self.power_port.name) - else: - power_port = None - return PowerOutlet( - device=device, - name=self.name, - power_port=power_port, - feed_leg=self.feed_leg - ) - - -class InterfaceTemplate(ComponentTemplateModel): - """ - A template for a physical data interface on a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='interface_templates' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=IFACE_TYPE_CHOICES, - default=IFACE_TYPE_10GE_SFP_PLUS - ) - mgmt_only = models.BooleanField( - default=False, - verbose_name='Management only' - ) - - objects = InterfaceManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - # TODO: Remove in v2.7 - @property - def form_factor(self): - """ - Backward-compatibility for form_factor - """ - return self.type - - # TODO: Remove in v2.7 - @form_factor.setter - def form_factor(self, value): - """ - Backward-compatibility for form_factor - """ - self.type = value - - def instantiate(self, device): - return Interface( - device=device, - name=self.name, - type=self.type, - mgmt_only=self.mgmt_only - ) - - -class FrontPortTemplate(ComponentTemplateModel): - """ - Template for a pass-through port on the front of a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='frontport_templates' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=PORT_TYPE_CHOICES - ) - rear_port = models.ForeignKey( - to='dcim.RearPortTemplate', - on_delete=models.CASCADE, - related_name='frontport_templates' - ) - rear_port_position = models.PositiveSmallIntegerField( - default=1, - validators=[MinValueValidator(1), MaxValueValidator(64)] - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = [ - ['device_type', 'name'], - ['rear_port', 'rear_port_position'], - ] - - def __str__(self): - return self.name - - def clean(self): - - # Validate rear port assignment - if self.rear_port.device_type != self.device_type: - raise ValidationError( - "Rear port ({}) must belong to the same device type".format(self.rear_port) - ) - - # Validate rear port position assignment - if self.rear_port_position > self.rear_port.positions: - raise ValidationError( - "Invalid rear port position ({}); rear port {} has only {} positions".format( - self.rear_port_position, self.rear_port.name, self.rear_port.positions - ) - ) - - def instantiate(self, device): - if self.rear_port: - rear_port = RearPort.objects.get(device=device, name=self.rear_port.name) - else: - rear_port = None - return FrontPort( - device=device, - name=self.name, - type=self.type, - rear_port=rear_port, - rear_port_position=self.rear_port_position - ) - - -class RearPortTemplate(ComponentTemplateModel): - """ - Template for a pass-through port on the rear of a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='rearport_templates' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=PORT_TYPE_CHOICES - ) - positions = models.PositiveSmallIntegerField( - default=1, - validators=[MinValueValidator(1), MaxValueValidator(64)] - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return RearPort( - device=device, - name=self.name, - type=self.type, - positions=self.positions - ) - - -class DeviceBayTemplate(ComponentTemplateModel): - """ - A template for a DeviceBay to be created for a new parent Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='device_bay_templates' - ) - name = models.CharField( - max_length=50 - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return DeviceBay( - device=device, - name=self.name - ) + return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD # @@ -1378,8 +1105,12 @@ class DeviceRole(ChangeLoggedModel): verbose_name='VM Role', help_text='Virtual machines may be assigned to this role' ) + description = models.CharField( + max_length=100, + blank=True, + ) - csv_headers = ['name', 'slug', 'color', 'vm_role'] + csv_headers = ['name', 'slug', 'color', 'vm_role', 'description'] class Meta: ordering = ['name'] @@ -1393,6 +1124,7 @@ class DeviceRole(ChangeLoggedModel): self.slug, self.color, self.vm_role, + self.description, ) @@ -1491,8 +1223,13 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): name = models.CharField( max_length=64, blank=True, - null=True, - unique=True + null=True + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True, + null=True ) serial = models.CharField( max_length=50, @@ -1526,16 +1263,16 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): verbose_name='Position (U)', help_text='The lowest-numbered unit occupied by the device' ) - face = models.PositiveSmallIntegerField( + face = models.CharField( + max_length=50, blank=True, - null=True, - choices=RACK_FACE_CHOICES, + choices=DeviceFaceChoices, verbose_name='Rack face' ) - status = models.PositiveSmallIntegerField( - choices=DEVICE_STATUS_CHOICES, - default=DEVICE_STATUS_ACTIVE, - verbose_name='Status' + status = models.CharField( + max_length=50, + choices=DeviceStatusChoices, + default=DeviceStatusChoices.STATUS_ACTIVE ) primary_ip4 = models.OneToOneField( to='ipam.IPAddress', @@ -1588,21 +1325,33 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): images = GenericRelation( to='extras.ImageAttachment' ) - - objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = [ 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', ] + clone_fields = [ + 'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster', + ] + + STATUS_CLASS_MAP = { + DeviceStatusChoices.STATUS_OFFLINE: 'warning', + DeviceStatusChoices.STATUS_ACTIVE: 'success', + DeviceStatusChoices.STATUS_PLANNED: 'info', + DeviceStatusChoices.STATUS_STAGED: 'primary', + DeviceStatusChoices.STATUS_FAILED: 'danger', + DeviceStatusChoices.STATUS_INVENTORY: 'default', + DeviceStatusChoices.STATUS_DECOMMISSIONING: 'warning', + } class Meta: - ordering = ['name'] - unique_together = [ - ['rack', 'position', 'face'], - ['virtual_chassis', 'vc_position'], - ] + ordering = ('_name', 'pk') # Name may be null + unique_together = ( + ('site', 'tenant', 'name'), # See validate_unique below + ('rack', 'position', 'face'), + ('virtual_chassis', 'vc_position'), + ) permissions = ( ('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'), @@ -1614,6 +1363,19 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) + def validate_unique(self, exclude=None): + + # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary + # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation + # of the uniqueness constraint without manual intervention. + if self.name and self.tenant is None: + if Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True): + raise ValidationError({ + 'name': 'A device with this name already exists.' + }) + + super().validate_unique(exclude) + def clean(self): super().clean() @@ -1625,7 +1387,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): }) if self.rack is None: - if self.face is not None: + if self.face: raise ValidationError({ 'face': "Cannot select a rack face without assigning a rack.", }) @@ -1635,7 +1397,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): }) # Validate position/face combination - if self.position and self.face is None: + if self.position and not self.face: raise ValidationError({ 'face': "Must specify rack face when defining rack position.", }) @@ -1650,7 +1412,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): try: # Child devices cannot be assigned to a rack face/unit - if self.device_type.is_child_device and self.face is not None: + if self.device_type.is_child_device and self.face: raise ValidationError({ 'face': "Child device types cannot be assigned to a rack face. This is an attribute of the " "parent device." @@ -1850,848 +1612,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): return Device.objects.filter(parent_bay__device=self.pk) def get_status_class(self): - return STATUS_CLASSES[self.status] - - -# -# Console ports -# - -class ConsolePort(CableTermination, ComponentModel): - """ - A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='consoleports' - ) - name = models.CharField( - max_length=50 - ) - connected_endpoint = models.OneToOneField( - to='dcim.ConsoleServerPort', - on_delete=models.SET_NULL, - related_name='connected_endpoint', - blank=True, - null=True - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.description, - ) - - -# -# Console server ports -# - -class ConsoleServerPort(CableTermination, ComponentModel): - """ - A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='consoleserverports' - ) - name = models.CharField( - max_length=50 - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'description'] - - class Meta: - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.description, - ) - - -# -# Power ports -# - -class PowerPort(CableTermination, ComponentModel): - """ - A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='powerports' - ) - name = models.CharField( - max_length=50 - ) - maximum_draw = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1)], - help_text="Maximum current draw (watts)" - ) - allocated_draw = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1)], - help_text="Allocated current draw (watts)" - ) - _connected_poweroutlet = models.OneToOneField( - to='dcim.PowerOutlet', - on_delete=models.SET_NULL, - related_name='connected_endpoint', - blank=True, - null=True - ) - _connected_powerfeed = models.OneToOneField( - to='dcim.PowerFeed', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'maximum_draw', 'allocated_draw', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.maximum_draw, - self.allocated_draw, - self.description, - ) - - @property - def connected_endpoint(self): - if self._connected_poweroutlet: - return self._connected_poweroutlet - return self._connected_powerfeed - - @connected_endpoint.setter - def connected_endpoint(self, value): - if value is None: - self._connected_poweroutlet = None - self._connected_powerfeed = None - elif isinstance(value, PowerOutlet): - self._connected_poweroutlet = value - self._connected_powerfeed = None - elif isinstance(value, PowerFeed): - self._connected_poweroutlet = None - self._connected_powerfeed = value - else: - raise ValueError( - "Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value)) - ) - - def get_power_draw(self): - """ - Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort. - """ - # Calculate aggregate draw of all child power outlets if no numbers have been defined manually - if self.allocated_draw is None and self.maximum_draw is None: - outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True) - utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( - maximum_draw_total=Sum('maximum_draw'), - allocated_draw_total=Sum('allocated_draw'), - ) - ret = { - 'allocated': utilization['allocated_draw_total'] or 0, - 'maximum': utilization['maximum_draw_total'] or 0, - 'outlet_count': len(outlet_ids), - 'legs': [], - } - - # Calculate per-leg aggregates for three-phase feeds - if self._connected_powerfeed and self._connected_powerfeed.phase == POWERFEED_PHASE_3PHASE: - for leg, leg_name in POWERFEED_LEG_CHOICES: - outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True) - utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( - maximum_draw_total=Sum('maximum_draw'), - allocated_draw_total=Sum('allocated_draw'), - ) - ret['legs'].append({ - 'name': leg_name, - 'allocated': utilization['allocated_draw_total'] or 0, - 'maximum': utilization['maximum_draw_total'] or 0, - 'outlet_count': len(outlet_ids), - }) - - return ret - - # Default to administratively defined values - return { - 'allocated': self.allocated_draw or 0, - 'maximum': self.maximum_draw or 0, - 'outlet_count': PowerOutlet.objects.filter(power_port=self).count(), - 'legs': [], - } - - -# -# Power outlets -# - -class PowerOutlet(CableTermination, ComponentModel): - """ - A physical power outlet (output) within a Device which provides power to a PowerPort. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='poweroutlets' - ) - name = models.CharField( - max_length=50 - ) - power_port = models.ForeignKey( - to='dcim.PowerPort', - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name='poweroutlets' - ) - feed_leg = models.PositiveSmallIntegerField( - choices=POWERFEED_LEG_CHOICES, - blank=True, - null=True, - help_text="Phase (for three-phase feeds)" - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'power_port', 'feed_leg', 'description'] - - class Meta: - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.power_port.name if self.power_port else None, - self.get_feed_leg_display(), - self.description, - ) - - def clean(self): - - # Validate power port assignment - if self.power_port and self.power_port.device != self.device: - raise ValidationError( - "Parent power port ({}) must belong to the same device".format(self.power_port) - ) - - -# -# Interfaces -# - -class Interface(CableTermination, ComponentModel): - """ - A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other - Interface. - """ - device = models.ForeignKey( - to='Device', - on_delete=models.CASCADE, - related_name='interfaces', - null=True, - blank=True - ) - virtual_machine = models.ForeignKey( - to='virtualization.VirtualMachine', - on_delete=models.CASCADE, - related_name='interfaces', - null=True, - blank=True - ) - name = models.CharField( - max_length=64 - ) - _connected_interface = models.OneToOneField( - to='self', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - _connected_circuittermination = models.OneToOneField( - to='circuits.CircuitTermination', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - lag = models.ForeignKey( - to='self', - on_delete=models.SET_NULL, - related_name='member_interfaces', - null=True, - blank=True, - verbose_name='Parent LAG' - ) - type = models.PositiveSmallIntegerField( - choices=IFACE_TYPE_CHOICES, - default=IFACE_TYPE_10GE_SFP_PLUS - ) - enabled = models.BooleanField( - default=True - ) - mac_address = MACAddressField( - null=True, - blank=True, - verbose_name='MAC Address' - ) - mtu = models.PositiveIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1), MaxValueValidator(65536)], - verbose_name='MTU' - ) - mgmt_only = models.BooleanField( - default=False, - verbose_name='OOB Management', - help_text='This interface is used only for out-of-band management' - ) - mode = models.PositiveSmallIntegerField( - choices=IFACE_MODE_CHOICES, - blank=True, - null=True - ) - untagged_vlan = models.ForeignKey( - to='ipam.VLAN', - on_delete=models.SET_NULL, - related_name='interfaces_as_untagged', - null=True, - blank=True, - verbose_name='Untagged VLAN' - ) - tagged_vlans = models.ManyToManyField( - to='ipam.VLAN', - related_name='interfaces_as_tagged', - blank=True, - verbose_name='Tagged VLANs' - ) - - objects = InterfaceManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = [ - 'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', - 'description', 'mode', - ] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse('dcim:interface', kwargs={'pk': self.pk}) - - def to_csv(self): - return ( - self.device.identifier if self.device else None, - self.virtual_machine.name if self.virtual_machine else None, - self.name, - self.lag.name if self.lag else None, - self.get_type_display(), - self.enabled, - self.mac_address, - self.mtu, - self.mgmt_only, - self.description, - self.get_mode_display(), - ) - - def clean(self): - - # An Interface must belong to a Device *or* to a VirtualMachine - if self.device and self.virtual_machine: - raise ValidationError("An interface cannot belong to both a device and a virtual machine.") - if not self.device and not self.virtual_machine: - raise ValidationError("An interface must belong to either a device or a virtual machine.") - - # VM interfaces must be virtual - if self.virtual_machine and self.type is not IFACE_TYPE_VIRTUAL: - raise ValidationError({ - 'type': "Virtual machines can only have virtual interfaces." - }) - - # Virtual interfaces cannot be connected - if self.type in NONCONNECTABLE_IFACE_TYPES and ( - self.cable or getattr(self, 'circuit_termination', False) - ): - raise ValidationError({ - 'type': "Virtual and wireless interfaces cannot be connected to another interface or circuit. " - "Disconnect the interface or choose a suitable type." - }) - - # An interface's LAG must belong to the same device (or VC master) - if self.lag and self.lag.device not in [self.device, self.device.get_vc_master()]: - raise ValidationError({ - 'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format( - self.lag.name, self.lag.device.name - ) - }) - - # A virtual interface cannot have a parent LAG - if self.type in NONCONNECTABLE_IFACE_TYPES and self.lag is not None: - raise ValidationError({ - 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_type_display()) - }) - - # Only a LAG can have LAG members - if self.type != IFACE_TYPE_LAG and self.member_interfaces.exists(): - raise ValidationError({ - 'type': "Cannot change interface type; it has LAG members ({}).".format( - ", ".join([iface.name for iface in self.member_interfaces.all()]) - ) - }) - - # Validate untagged VLAN - if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]: - raise ValidationError({ - 'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent " - "device/VM, or it must be global".format(self.untagged_vlan) - }) - - def save(self, *args, **kwargs): - - # Remove untagged VLAN assignment for non-802.1Q interfaces - if self.mode is None: - self.untagged_vlan = None - - # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.) - if self.pk and self.mode is not IFACE_MODE_TAGGED: - self.tagged_vlans.clear() - - return super().save(*args, **kwargs) - - def to_objectchange(self, action): - # Annotate the parent Device/VM - try: - parent_obj = self.device or self.virtual_machine - except ObjectDoesNotExist: - parent_obj = None - - return ObjectChange( - changed_object=self, - object_repr=str(self), - action=action, - related_object=parent_obj, - object_data=serialize_object(self) - ) - - # TODO: Remove in v2.7 - @property - def form_factor(self): - """ - Backward-compatibility for form_factor - """ - return self.type - - # TODO: Remove in v2.7 - @form_factor.setter - def form_factor(self, value): - """ - Backward-compatibility for form_factor - """ - self.type = value - - @property - def connected_endpoint(self): - if self._connected_interface: - return self._connected_interface - return self._connected_circuittermination - - @connected_endpoint.setter - def connected_endpoint(self, value): - from circuits.models import CircuitTermination - - if value is None: - self._connected_interface = None - self._connected_circuittermination = None - elif isinstance(value, Interface): - self._connected_interface = value - self._connected_circuittermination = None - elif isinstance(value, CircuitTermination): - self._connected_interface = None - self._connected_circuittermination = value - else: - raise ValueError( - "Connected endpoint must be an Interface or CircuitTermination, not {}.".format(type(value)) - ) - - @property - def parent(self): - return self.device or self.virtual_machine - - @property - def is_connectable(self): - return self.type not in NONCONNECTABLE_IFACE_TYPES - - @property - def is_virtual(self): - return self.type in VIRTUAL_IFACE_TYPES - - @property - def is_wireless(self): - return self.type in WIRELESS_IFACE_TYPES - - @property - def is_lag(self): - return self.type == IFACE_TYPE_LAG - - @property - def count_ipaddresses(self): - return self.ip_addresses.count() - - -# -# Pass-through ports -# - -class FrontPort(CableTermination, ComponentModel): - """ - A pass-through port on the front of a Device. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='frontports' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=PORT_TYPE_CHOICES - ) - rear_port = models.ForeignKey( - to='dcim.RearPort', - on_delete=models.CASCADE, - related_name='frontports' - ) - rear_port_position = models.PositiveSmallIntegerField( - default=1, - validators=[MinValueValidator(1), MaxValueValidator(64)] - ) - - is_path_endpoint = False - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = [ - ['device', 'name'], - ['rear_port', 'rear_port_position'], - ] - - def __str__(self): - return self.name - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.get_type_display(), - self.rear_port.name, - self.rear_port_position, - self.description, - ) - - def clean(self): - - # Validate rear port assignment - if self.rear_port.device != self.device: - raise ValidationError( - "Rear port ({}) must belong to the same device".format(self.rear_port) - ) - - # Validate rear port position assignment - if self.rear_port_position > self.rear_port.positions: - raise ValidationError( - "Invalid rear port position ({}); rear port {} has only {} positions".format( - self.rear_port_position, self.rear_port.name, self.rear_port.positions - ) - ) - - -class RearPort(CableTermination, ComponentModel): - """ - A pass-through port on the rear of a Device. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='rearports' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=PORT_TYPE_CHOICES - ) - positions = models.PositiveSmallIntegerField( - default=1, - validators=[MinValueValidator(1), MaxValueValidator(64)] - ) - - is_path_endpoint = False - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'type', 'positions', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.get_type_display(), - self.positions, - self.description, - ) - - -# -# Device bays -# - -class DeviceBay(ComponentModel): - """ - An empty space within a Device which can house a child device - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='device_bays' - ) - name = models.CharField( - max_length=50, - verbose_name='Name' - ) - installed_device = models.OneToOneField( - to='dcim.Device', - on_delete=models.SET_NULL, - related_name='parent_bay', - blank=True, - null=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'installed_device', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return '{} - {}'.format(self.device.name, self.name) - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.installed_device.identifier if self.installed_device else None, - self.description, - ) - - def clean(self): - - # Validate that the parent Device can have DeviceBays - if not self.device.device_type.is_parent_device: - raise ValidationError("This type of device ({}) does not support device bays.".format( - self.device.device_type - )) - - # Cannot install a device into itself, obviously - if self.device == self.installed_device: - raise ValidationError("Cannot install a device into itself.") - - # Check that the installed device is not already installed elsewhere - if self.installed_device: - current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first() - if current_bay and current_bay != self: - raise ValidationError({ - 'installed_device': "Cannot install the specified device; device is already installed in {}".format( - current_bay - ) - }) - - -# -# Inventory items -# - -class InventoryItem(ComponentModel): - """ - An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. - InventoryItems are used only for inventory purposes. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='inventory_items' - ) - parent = models.ForeignKey( - to='self', - on_delete=models.CASCADE, - related_name='child_items', - blank=True, - null=True - ) - name = models.CharField( - max_length=50, - verbose_name='Name' - ) - manufacturer = models.ForeignKey( - to='dcim.Manufacturer', - on_delete=models.PROTECT, - related_name='inventory_items', - blank=True, - null=True - ) - part_id = models.CharField( - max_length=50, - verbose_name='Part ID', - blank=True - ) - serial = models.CharField( - max_length=50, - verbose_name='Serial number', - blank=True - ) - asset_tag = models.CharField( - max_length=50, - unique=True, - blank=True, - null=True, - verbose_name='Asset tag', - help_text='A unique tag used to identify this item' - ) - discovered = models.BooleanField( - default=False, - verbose_name='Discovered' - ) - - tags = TaggableManager(through=TaggedItem) - - csv_headers = [ - 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', - ] - - class Meta: - ordering = ['device__id', 'parent__id', 'name'] - unique_together = ['device', 'parent', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.name or '{{{}}}'.format(self.device.pk), - self.name, - self.manufacturer.name if self.manufacturer else None, - self.part_id, - self.serial, - self.asset_tag, - self.discovered, - self.description, - ) + return self.STATUS_CLASS_MAP.get(self.status) # @@ -2760,6 +1681,207 @@ class VirtualChassis(ChangeLoggedModel): ) +# +# Power +# + +class PowerPanel(ChangeLoggedModel): + """ + A distribution point for electrical power; e.g. a data center RPP. + """ + site = models.ForeignKey( + to='Site', + on_delete=models.PROTECT + ) + rack_group = models.ForeignKey( + to='RackGroup', + on_delete=models.PROTECT, + blank=True, + null=True + ) + name = models.CharField( + max_length=50 + ) + + csv_headers = ['site', 'rack_group_name', 'name'] + + class Meta: + ordering = ['site', 'name'] + unique_together = ['site', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('dcim:powerpanel', args=[self.pk]) + + def to_csv(self): + return ( + self.site.name, + self.rack_group.name if self.rack_group else None, + self.name, + ) + + def clean(self): + + # RackGroup must belong to assigned Site + if self.rack_group and self.rack_group.site != self.site: + raise ValidationError("Rack group {} ({}) is in a different site than {}".format( + self.rack_group, self.rack_group.site, self.site + )) + + +class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): + """ + An electrical circuit delivered from a PowerPanel. + """ + power_panel = models.ForeignKey( + to='PowerPanel', + on_delete=models.PROTECT, + related_name='powerfeeds' + ) + rack = models.ForeignKey( + to='Rack', + on_delete=models.PROTECT, + blank=True, + null=True + ) + connected_endpoint = models.OneToOneField( + to='dcim.PowerPort', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + name = models.CharField( + max_length=50 + ) + status = models.CharField( + max_length=50, + choices=PowerFeedStatusChoices, + default=PowerFeedStatusChoices.STATUS_ACTIVE + ) + type = models.CharField( + max_length=50, + choices=PowerFeedTypeChoices, + default=PowerFeedTypeChoices.TYPE_PRIMARY + ) + supply = models.CharField( + max_length=50, + choices=PowerFeedSupplyChoices, + default=PowerFeedSupplyChoices.SUPPLY_AC + ) + phase = models.CharField( + max_length=50, + choices=PowerFeedPhaseChoices, + default=PowerFeedPhaseChoices.PHASE_SINGLE + ) + voltage = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1)], + default=POWERFEED_VOLTAGE_DEFAULT + ) + amperage = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1)], + default=POWERFEED_AMPERAGE_DEFAULT + ) + max_utilization = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(100)], + default=POWERFEED_MAX_UTILIZATION_DEFAULT, + help_text="Maximum permissible draw (percentage)" + ) + available_power = models.PositiveIntegerField( + default=0, + editable=False + ) + comments = models.TextField( + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) + + tags = TaggableManager(through=TaggedItem) + + csv_headers = [ + 'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage', + 'amperage', 'max_utilization', 'comments', + ] + clone_fields = [ + 'power_panel', 'rack', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', + 'available_power', + ] + + STATUS_CLASS_MAP = { + PowerFeedStatusChoices.STATUS_OFFLINE: 'warning', + PowerFeedStatusChoices.STATUS_ACTIVE: 'success', + PowerFeedStatusChoices.STATUS_PLANNED: 'info', + PowerFeedStatusChoices.STATUS_FAILED: 'danger', + } + + TYPE_CLASS_MAP = { + PowerFeedTypeChoices.TYPE_PRIMARY: 'success', + PowerFeedTypeChoices.TYPE_REDUNDANT: 'info', + } + + class Meta: + ordering = ['power_panel', 'name'] + unique_together = ['power_panel', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('dcim:powerfeed', args=[self.pk]) + + def to_csv(self): + return ( + self.power_panel.site.name, + self.power_panel.name, + self.rack.group.name if self.rack and self.rack.group else None, + self.rack.name if self.rack else None, + self.name, + self.get_status_display(), + self.get_type_display(), + self.get_supply_display(), + self.get_phase_display(), + self.voltage, + self.amperage, + self.max_utilization, + self.comments, + ) + + def clean(self): + + # Rack must belong to same Site as PowerPanel + if self.rack and self.rack.site != self.power_panel.site: + raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format( + self.rack, self.rack.site, self.power_panel, self.power_panel.site + )) + + def save(self, *args, **kwargs): + + # Cache the available_power property on the instance + kva = self.voltage * self.amperage * (self.max_utilization / 100) + if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE: + self.available_power = round(kva * 1.732) + else: + self.available_power = round(kva) + + super().save(*args, **kwargs) + + def get_type_class(self): + return self.TYPE_CLASS_MAP.get(self.type) + + def get_status_class(self): + return self.STATUS_CLASS_MAP.get(self.status) + + # # Cables # @@ -2770,7 +1892,7 @@ class Cable(ChangeLoggedModel): """ termination_a_type = models.ForeignKey( to=ContentType, - limit_choices_to={'model__in': CABLE_TERMINATION_TYPES}, + limit_choices_to=CABLE_TERMINATION_MODELS, on_delete=models.PROTECT, related_name='+' ) @@ -2781,7 +1903,7 @@ class Cable(ChangeLoggedModel): ) termination_b_type = models.ForeignKey( to=ContentType, - limit_choices_to={'model__in': CABLE_TERMINATION_TYPES}, + limit_choices_to=CABLE_TERMINATION_MODELS, on_delete=models.PROTECT, related_name='+' ) @@ -2790,14 +1912,15 @@ class Cable(ChangeLoggedModel): ct_field='termination_b_type', fk_field='termination_b_id' ) - type = models.PositiveSmallIntegerField( - choices=CABLE_TYPE_CHOICES, - blank=True, - null=True + type = models.CharField( + max_length=50, + choices=CableTypeChoices, + blank=True ) - status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - default=CONNECTION_STATUS_CONNECTED + status = models.CharField( + max_length=50, + choices=CableStatusChoices, + default=CableStatusChoices.STATUS_CONNECTED ) label = models.CharField( max_length=100, @@ -2810,10 +1933,10 @@ class Cable(ChangeLoggedModel): blank=True, null=True ) - length_unit = models.PositiveSmallIntegerField( - choices=CABLE_LENGTH_UNIT_CHOICES, + length_unit = models.CharField( + max_length=50, + choices=CableLengthUnitChoices, blank=True, - null=True ) # Stores the normalized length (in meters) for database ordering _abs_length = models.DecimalField( @@ -2844,6 +1967,12 @@ class Cable(ChangeLoggedModel): 'color', 'length', 'length_unit', ] + STATUS_CLASS_MAP = { + CableStatusChoices.STATUS_CONNECTED: 'success', + CableStatusChoices.STATUS_PLANNED: 'info', + CableStatusChoices.STATUS_DECOMMISSIONING: 'warning', + } + class Meta: ordering = ['pk'] unique_together = ( @@ -2851,15 +1980,14 @@ class Cable(ChangeLoggedModel): ('termination_b_type', 'termination_b_id'), ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # A copy of the PK to be used by __str__ in case the object is deleted + self._pk = self.pk + def __str__(self): - if self.label: - return self.label - - # Save a copy of the PK on the instance since it's nullified if .delete() is called - if not hasattr(self, 'id_string'): - self.id_string = '#{}'.format(self.pk) - - return self.id_string + return self.label or '#{}'.format(self._pk) def get_absolute_url(self): return reverse('dcim:cable', args=[self.pk]) @@ -2945,16 +2073,18 @@ class Cable(ChangeLoggedModel): )) # Validate length and length_unit - if self.length is not None and self.length_unit is None: + if self.length is not None and not self.length_unit: raise ValidationError("Must specify a unit when setting a cable length") elif self.length is None: - self.length_unit = None + self.length_unit = '' def save(self, *args, **kwargs): # Store the given length (if any) in meters for use in database ordering if self.length and self.length_unit: self._abs_length = to_meters(self.length, self.length_unit) + else: + self._abs_length = None # Store the parent Device for the A and B terminations (if applicable) to enable filtering if hasattr(self.termination_a, 'device'): @@ -2964,6 +2094,9 @@ class Cable(ChangeLoggedModel): super().save(*args, **kwargs) + # Update the private pk used in __str__ in case this is a new object (i.e. just got its pk) + self._pk = self.pk + def to_csv(self): return ( '{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model), @@ -2979,7 +2112,7 @@ class Cable(ChangeLoggedModel): ) def get_status_class(self): - return 'success' if self.status else 'info' + return self.STATUS_CLASS_MAP.get(self.status) def get_compatible_types(self): """ @@ -2998,197 +2131,16 @@ class Cable(ChangeLoggedModel): b_path = self.termination_a.trace() # Determine overall path status (connected or planned) - if self.status == CONNECTION_STATUS_PLANNED: - path_status = CONNECTION_STATUS_PLANNED - else: - path_status = CONNECTION_STATUS_CONNECTED + if self.status == CableStatusChoices.STATUS_CONNECTED: + path_status = True for segment in a_path[1:] + b_path[1:]: - if segment[1] is None or segment[1].status == CONNECTION_STATUS_PLANNED: - path_status = CONNECTION_STATUS_PLANNED + if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED: + path_status = False break + else: + path_status = False a_endpoint = a_path[-1][2] b_endpoint = b_path[-1][2] return a_endpoint, b_endpoint, path_status - - -# -# Power -# - -class PowerPanel(ChangeLoggedModel): - """ - A distribution point for electrical power; e.g. a data center RPP. - """ - site = models.ForeignKey( - to='Site', - on_delete=models.PROTECT - ) - rack_group = models.ForeignKey( - to='RackGroup', - on_delete=models.PROTECT, - blank=True, - null=True - ) - name = models.CharField( - max_length=50 - ) - - csv_headers = ['site', 'rack_group_name', 'name'] - - class Meta: - ordering = ['site', 'name'] - unique_together = ['site', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse('dcim:powerpanel', args=[self.pk]) - - def to_csv(self): - return ( - self.site.name, - self.rack_group.name if self.rack_group else None, - self.name, - ) - - def clean(self): - - # RackGroup must belong to assigned Site - if self.rack_group and self.rack_group.site != self.site: - raise ValidationError("Rack group {} ({}) is in a different site than {}".format( - self.rack_group, self.rack_group.site, self.site - )) - - -class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): - """ - An electrical circuit delivered from a PowerPanel. - """ - power_panel = models.ForeignKey( - to='PowerPanel', - on_delete=models.PROTECT, - related_name='powerfeeds' - ) - rack = models.ForeignKey( - to='Rack', - on_delete=models.PROTECT, - blank=True, - null=True - ) - connected_endpoint = models.OneToOneField( - to='dcim.PowerPort', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - name = models.CharField( - max_length=50 - ) - status = models.PositiveSmallIntegerField( - choices=POWERFEED_STATUS_CHOICES, - default=POWERFEED_STATUS_ACTIVE - ) - type = models.PositiveSmallIntegerField( - choices=POWERFEED_TYPE_CHOICES, - default=POWERFEED_TYPE_PRIMARY - ) - supply = models.PositiveSmallIntegerField( - choices=POWERFEED_SUPPLY_CHOICES, - default=POWERFEED_SUPPLY_AC - ) - phase = models.PositiveSmallIntegerField( - choices=POWERFEED_PHASE_CHOICES, - default=POWERFEED_PHASE_SINGLE - ) - voltage = models.PositiveSmallIntegerField( - validators=[MinValueValidator(1)], - default=120 - ) - amperage = models.PositiveSmallIntegerField( - validators=[MinValueValidator(1)], - default=20 - ) - max_utilization = models.PositiveSmallIntegerField( - validators=[MinValueValidator(1), MaxValueValidator(100)], - default=80, - help_text="Maximum permissible draw (percentage)" - ) - available_power = models.PositiveSmallIntegerField( - default=0, - editable=False - ) - comments = models.TextField( - blank=True - ) - custom_field_values = GenericRelation( - to='extras.CustomFieldValue', - content_type_field='obj_type', - object_id_field='obj_id' - ) - - tags = TaggableManager(through=TaggedItem) - - csv_headers = [ - 'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage', - 'amperage', 'max_utilization', 'comments', - ] - - class Meta: - ordering = ['power_panel', 'name'] - unique_together = ['power_panel', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse('dcim:powerfeed', args=[self.pk]) - - def to_csv(self): - return ( - self.power_panel.site.name, - self.power_panel.name, - self.rack.group.name if self.rack and self.rack.group else None, - self.rack.name if self.rack else None, - self.name, - self.get_status_display(), - self.get_type_display(), - self.get_supply_display(), - self.get_phase_display(), - self.voltage, - self.amperage, - self.max_utilization, - self.comments, - ) - - def clean(self): - - # Rack must belong to same Site as PowerPanel - if self.rack and self.rack.site != self.power_panel.site: - raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format( - self.rack, self.rack.site, self.power_panel, self.power_panel.site - )) - - def save(self, *args, **kwargs): - - # Cache the available_power property on the instance - kva = self.voltage * self.amperage * (self.max_utilization / 100) - if self.phase == POWERFEED_PHASE_3PHASE: - self.available_power = round(kva * 1.732) - else: - self.available_power = round(kva) - - super().save(*args, **kwargs) - - def get_type_class(self): - return STATUS_CLASSES[self.type] - - def get_status_class(self): - return STATUS_CLASSES[self.status] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py new file mode 100644 index 000000000..faa42b035 --- /dev/null +++ b/netbox/dcim/models/device_component_templates.py @@ -0,0 +1,427 @@ +from django.core.exceptions import ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from dcim.choices import * +from dcim.constants import * +from extras.models import ObjectChange +from utilities.fields import NaturalOrderingField +from utilities.ordering import naturalize_interface +from utilities.utils import serialize_object +from .device_components import ( + ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort, +) + + +__all__ = ( + 'ConsolePortTemplate', + 'ConsoleServerPortTemplate', + 'DeviceBayTemplate', + 'FrontPortTemplate', + 'InterfaceTemplate', + 'PowerOutletTemplate', + 'PowerPortTemplate', + 'RearPortTemplate', +) + + +class ComponentTemplateModel(models.Model): + + class Meta: + abstract = True + + def instantiate(self, device): + """ + Instantiate a new component on the specified Device. + """ + raise NotImplementedError() + + def to_objectchange(self, action): + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + related_object=self.device_type, + object_data=serialize_object(self) + ) + + +class ConsolePortTemplate(ComponentTemplateModel): + """ + A template for a ConsolePort to be created for a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='consoleport_templates' + ) + name = models.CharField( + max_length=50 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=ConsolePortTypeChoices, + blank=True + ) + + class Meta: + ordering = ('device_type', '_name') + unique_together = ('device_type', 'name') + + def __str__(self): + return self.name + + def instantiate(self, device): + return ConsolePort( + device=device, + name=self.name, + type=self.type + ) + + +class ConsoleServerPortTemplate(ComponentTemplateModel): + """ + A template for a ConsoleServerPort to be created for a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='consoleserverport_templates' + ) + name = models.CharField( + max_length=50 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=ConsolePortTypeChoices, + blank=True + ) + + class Meta: + ordering = ('device_type', '_name') + unique_together = ('device_type', 'name') + + def __str__(self): + return self.name + + def instantiate(self, device): + return ConsoleServerPort( + device=device, + name=self.name, + type=self.type + ) + + +class PowerPortTemplate(ComponentTemplateModel): + """ + A template for a PowerPort to be created for a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='powerport_templates' + ) + name = models.CharField( + max_length=50 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=PowerPortTypeChoices, + blank=True + ) + maximum_draw = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1)], + help_text="Maximum power draw (watts)" + ) + allocated_draw = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1)], + help_text="Allocated power draw (watts)" + ) + + class Meta: + ordering = ('device_type', '_name') + unique_together = ('device_type', 'name') + + def __str__(self): + return self.name + + def instantiate(self, device): + return PowerPort( + device=device, + name=self.name, + type=self.type, + maximum_draw=self.maximum_draw, + allocated_draw=self.allocated_draw + ) + + +class PowerOutletTemplate(ComponentTemplateModel): + """ + A template for a PowerOutlet to be created for a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='poweroutlet_templates' + ) + name = models.CharField( + max_length=50 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=PowerOutletTypeChoices, + blank=True + ) + power_port = models.ForeignKey( + to='dcim.PowerPortTemplate', + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name='poweroutlet_templates' + ) + feed_leg = models.CharField( + max_length=50, + choices=PowerOutletFeedLegChoices, + blank=True, + help_text="Phase (for three-phase feeds)" + ) + + class Meta: + ordering = ('device_type', '_name') + unique_together = ('device_type', 'name') + + def __str__(self): + return self.name + + def clean(self): + + # Validate power port assignment + if self.power_port and self.power_port.device_type != self.device_type: + raise ValidationError( + "Parent power port ({}) must belong to the same device type".format(self.power_port) + ) + + def instantiate(self, device): + if self.power_port: + power_port = PowerPort.objects.get(device=device, name=self.power_port.name) + else: + power_port = None + return PowerOutlet( + device=device, + name=self.name, + type=self.type, + power_port=power_port, + feed_leg=self.feed_leg + ) + + +class InterfaceTemplate(ComponentTemplateModel): + """ + A template for a physical data interface on a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='interface_templates' + ) + name = models.CharField( + max_length=64 + ) + _name = NaturalOrderingField( + target_field='name', + naturalize_function=naturalize_interface, + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=InterfaceTypeChoices + ) + mgmt_only = models.BooleanField( + default=False, + verbose_name='Management only' + ) + + class Meta: + ordering = ('device_type', '_name') + unique_together = ('device_type', 'name') + + def __str__(self): + return self.name + + def instantiate(self, device): + return Interface( + device=device, + name=self.name, + type=self.type, + mgmt_only=self.mgmt_only + ) + + +class FrontPortTemplate(ComponentTemplateModel): + """ + Template for a pass-through port on the front of a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='frontport_templates' + ) + name = models.CharField( + max_length=64 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=PortTypeChoices + ) + rear_port = models.ForeignKey( + to='dcim.RearPortTemplate', + on_delete=models.CASCADE, + related_name='frontport_templates' + ) + rear_port_position = models.PositiveSmallIntegerField( + default=1, + validators=[MinValueValidator(1), MaxValueValidator(64)] + ) + + class Meta: + ordering = ('device_type', '_name') + unique_together = ( + ('device_type', 'name'), + ('rear_port', 'rear_port_position'), + ) + + def __str__(self): + return self.name + + def clean(self): + + # Validate rear port assignment + if self.rear_port.device_type != self.device_type: + raise ValidationError( + "Rear port ({}) must belong to the same device type".format(self.rear_port) + ) + + # Validate rear port position assignment + if self.rear_port_position > self.rear_port.positions: + raise ValidationError( + "Invalid rear port position ({}); rear port {} has only {} positions".format( + self.rear_port_position, self.rear_port.name, self.rear_port.positions + ) + ) + + def instantiate(self, device): + if self.rear_port: + rear_port = RearPort.objects.get(device=device, name=self.rear_port.name) + else: + rear_port = None + return FrontPort( + device=device, + name=self.name, + type=self.type, + rear_port=rear_port, + rear_port_position=self.rear_port_position + ) + + +class RearPortTemplate(ComponentTemplateModel): + """ + Template for a pass-through port on the rear of a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='rearport_templates' + ) + name = models.CharField( + max_length=64 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=PortTypeChoices + ) + positions = models.PositiveSmallIntegerField( + default=1, + validators=[MinValueValidator(1), MaxValueValidator(64)] + ) + + class Meta: + ordering = ('device_type', '_name') + unique_together = ('device_type', 'name') + + def __str__(self): + return self.name + + def instantiate(self, device): + return RearPort( + device=device, + name=self.name, + type=self.type, + positions=self.positions + ) + + +class DeviceBayTemplate(ComponentTemplateModel): + """ + A template for a DeviceBay to be created for a new parent Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='device_bay_templates' + ) + name = models.CharField( + max_length=50 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + + class Meta: + ordering = ('device_type', '_name') + unique_together = ('device_type', 'name') + + def __str__(self): + return self.name + + def instantiate(self, device): + return DeviceBay( + device=device, + name=self.name + ) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py new file mode 100644 index 000000000..9a3d608d7 --- /dev/null +++ b/netbox/dcim/models/device_components.py @@ -0,0 +1,1074 @@ +from django.contrib.contenttypes.fields import GenericRelation +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django.db.models import Sum +from django.urls import reverse +from taggit.managers import TaggableManager + +from dcim.choices import * +from dcim.constants import * +from dcim.exceptions import LoopDetected +from dcim.fields import MACAddressField +from extras.models import ObjectChange, TaggedItem +from utilities.fields import NaturalOrderingField +from utilities.ordering import naturalize_interface +from utilities.utils import serialize_object +from virtualization.choices import VMInterfaceTypeChoices + + +__all__ = ( + 'CableTermination', + 'ConsolePort', + 'ConsoleServerPort', + 'DeviceBay', + 'FrontPort', + 'Interface', + 'InventoryItem', + 'PowerOutlet', + 'PowerPort', + 'RearPort', +) + + +class ComponentModel(models.Model): + description = models.CharField( + max_length=100, + blank=True + ) + + class Meta: + abstract = True + + def to_objectchange(self, action): + # Annotate the parent Device/VM + try: + parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None) + except ObjectDoesNotExist: + # The parent device/VM has already been deleted + parent = None + + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + related_object=parent, + object_data=serialize_object(self) + ) + + @property + def parent(self): + return getattr(self, 'device', None) + + +class CableTermination(models.Model): + cable = models.ForeignKey( + to='dcim.Cable', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + + # Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted. + _cabled_as_a = GenericRelation( + to='dcim.Cable', + content_type_field='termination_a_type', + object_id_field='termination_a_id' + ) + _cabled_as_b = GenericRelation( + to='dcim.Cable', + content_type_field='termination_b_type', + object_id_field='termination_b_id' + ) + + is_path_endpoint = True + + class Meta: + abstract = True + + def trace(self, position=1, follow_circuits=False, cable_history=None): + """ + Return a list representing a complete cable path, with each individual segment represented as a three-tuple: + [ + (termination A, cable, termination B), + (termination C, cable, termination D), + (termination E, cable, termination F) + ] + """ + def get_peer_port(termination, position=1, follow_circuits=False): + from circuits.models import CircuitTermination + + # Map a front port to its corresponding rear port + if isinstance(termination, FrontPort): + return termination.rear_port, termination.rear_port_position + + # Map a rear port/position to its corresponding front port + elif isinstance(termination, RearPort): + if position not in range(1, termination.positions + 1): + raise Exception("Invalid position for {} ({} positions): {})".format( + termination, termination.positions, position + )) + try: + peer_port = FrontPort.objects.get( + rear_port=termination, + rear_port_position=position, + ) + return peer_port, 1 + except ObjectDoesNotExist: + return None, None + + # Follow a circuit to its other termination + elif isinstance(termination, CircuitTermination) and follow_circuits: + peer_termination = termination.get_peer_termination() + if peer_termination is None: + return None, None + return peer_termination, position + + # Termination is not a pass-through port + else: + return None, None + + if not self.cable: + return [(self, None, None)] + + # Record cable history to detect loops + if cable_history is None: + cable_history = [] + elif self.cable in cable_history: + raise LoopDetected() + cable_history.append(self.cable) + + far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a + path = [(self, self.cable, far_end)] + + peer_port, position = get_peer_port(far_end, position, follow_circuits) + if peer_port is None: + return path + + try: + next_segment = peer_port.trace(position, follow_circuits, cable_history) + except LoopDetected: + return path + + if next_segment is None: + return path + [(peer_port, None, None)] + + return path + next_segment + + def get_cable_peer(self): + if self.cable is None: + return None + if self._cabled_as_a.exists(): + return self.cable.termination_b + if self._cabled_as_b.exists(): + return self.cable.termination_a + + +# +# Console ports +# + +class ConsolePort(CableTermination, ComponentModel): + """ + A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='consoleports' + ) + name = models.CharField( + max_length=50 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=ConsolePortTypeChoices, + blank=True + ) + connected_endpoint = models.OneToOneField( + to='dcim.ConsoleServerPort', + on_delete=models.SET_NULL, + related_name='connected_endpoint', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'description'] + + class Meta: + ordering = ('device', '_name') + unique_together = ('device', 'name') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.type, + self.description, + ) + + +# +# Console server ports +# + +class ConsoleServerPort(CableTermination, ComponentModel): + """ + A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='consoleserverports' + ) + name = models.CharField( + max_length=50 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=ConsolePortTypeChoices, + blank=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'description'] + + class Meta: + ordering = ('device', '_name') + unique_together = ('device', 'name') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.type, + self.description, + ) + + +# +# Power ports +# + +class PowerPort(CableTermination, ComponentModel): + """ + A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='powerports' + ) + name = models.CharField( + max_length=50 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=PowerPortTypeChoices, + blank=True + ) + maximum_draw = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1)], + help_text="Maximum power draw (watts)" + ) + allocated_draw = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1)], + help_text="Allocated power draw (watts)" + ) + _connected_poweroutlet = models.OneToOneField( + to='dcim.PowerOutlet', + on_delete=models.SET_NULL, + related_name='connected_endpoint', + blank=True, + null=True + ) + _connected_powerfeed = models.OneToOneField( + to='dcim.PowerFeed', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description'] + + class Meta: + ordering = ('device', '_name') + unique_together = ('device', 'name') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.get_type_display(), + self.maximum_draw, + self.allocated_draw, + self.description, + ) + + @property + def connected_endpoint(self): + """ + Return the connected PowerOutlet, if it exists, or the connected PowerFeed, if it exists. We have to check for + ObjectDoesNotExist in case the referenced object has been deleted from the database. + """ + try: + if self._connected_poweroutlet: + return self._connected_poweroutlet + except ObjectDoesNotExist: + pass + try: + if self._connected_powerfeed: + return self._connected_powerfeed + except ObjectDoesNotExist: + pass + return None + + @connected_endpoint.setter + def connected_endpoint(self, value): + # TODO: Fix circular import + from . import PowerFeed + + if value is None: + self._connected_poweroutlet = None + self._connected_powerfeed = None + elif isinstance(value, PowerOutlet): + self._connected_poweroutlet = value + self._connected_powerfeed = None + elif isinstance(value, PowerFeed): + self._connected_poweroutlet = None + self._connected_powerfeed = value + else: + raise ValueError( + "Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value)) + ) + + def get_power_draw(self): + """ + Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort. + """ + # Calculate aggregate draw of all child power outlets if no numbers have been defined manually + if self.allocated_draw is None and self.maximum_draw is None: + outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True) + utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( + maximum_draw_total=Sum('maximum_draw'), + allocated_draw_total=Sum('allocated_draw'), + ) + ret = { + 'allocated': utilization['allocated_draw_total'] or 0, + 'maximum': utilization['maximum_draw_total'] or 0, + 'outlet_count': len(outlet_ids), + 'legs': [], + } + + # Calculate per-leg aggregates for three-phase feeds + if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE: + for leg, leg_name in PowerOutletFeedLegChoices: + outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True) + utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( + maximum_draw_total=Sum('maximum_draw'), + allocated_draw_total=Sum('allocated_draw'), + ) + ret['legs'].append({ + 'name': leg_name, + 'allocated': utilization['allocated_draw_total'] or 0, + 'maximum': utilization['maximum_draw_total'] or 0, + 'outlet_count': len(outlet_ids), + }) + + return ret + + # Default to administratively defined values + return { + 'allocated': self.allocated_draw or 0, + 'maximum': self.maximum_draw or 0, + 'outlet_count': PowerOutlet.objects.filter(power_port=self).count(), + 'legs': [], + } + + +# +# Power outlets +# + +class PowerOutlet(CableTermination, ComponentModel): + """ + A physical power outlet (output) within a Device which provides power to a PowerPort. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='poweroutlets' + ) + name = models.CharField( + max_length=50 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=PowerOutletTypeChoices, + blank=True + ) + power_port = models.ForeignKey( + to='dcim.PowerPort', + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name='poweroutlets' + ) + feed_leg = models.CharField( + max_length=50, + choices=PowerOutletFeedLegChoices, + blank=True, + help_text="Phase (for three-phase feeds)" + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description'] + + class Meta: + ordering = ('device', '_name') + unique_together = ('device', 'name') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.get_type_display(), + self.power_port.name if self.power_port else None, + self.get_feed_leg_display(), + self.description, + ) + + def clean(self): + + # Validate power port assignment + if self.power_port and self.power_port.device != self.device: + raise ValidationError( + "Parent power port ({}) must belong to the same device".format(self.power_port) + ) + + +# +# Interfaces +# + +class Interface(CableTermination, ComponentModel): + """ + A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other + Interface. + """ + device = models.ForeignKey( + to='Device', + on_delete=models.CASCADE, + related_name='interfaces', + null=True, + blank=True + ) + virtual_machine = models.ForeignKey( + to='virtualization.VirtualMachine', + on_delete=models.CASCADE, + related_name='interfaces', + null=True, + blank=True + ) + name = models.CharField( + max_length=64 + ) + _name = NaturalOrderingField( + target_field='name', + naturalize_function=naturalize_interface, + max_length=100, + blank=True + ) + _connected_interface = models.OneToOneField( + to='self', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + _connected_circuittermination = models.OneToOneField( + to='circuits.CircuitTermination', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + lag = models.ForeignKey( + to='self', + on_delete=models.SET_NULL, + related_name='member_interfaces', + null=True, + blank=True, + verbose_name='Parent LAG' + ) + type = models.CharField( + max_length=50, + choices=InterfaceTypeChoices + ) + enabled = models.BooleanField( + default=True + ) + mac_address = MACAddressField( + null=True, + blank=True, + verbose_name='MAC Address' + ) + mtu = models.PositiveIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1), MaxValueValidator(65536)], + verbose_name='MTU' + ) + mgmt_only = models.BooleanField( + default=False, + verbose_name='OOB Management', + help_text='This interface is used only for out-of-band management' + ) + mode = models.CharField( + max_length=50, + choices=InterfaceModeChoices, + blank=True, + ) + untagged_vlan = models.ForeignKey( + to='ipam.VLAN', + on_delete=models.SET_NULL, + related_name='interfaces_as_untagged', + null=True, + blank=True, + verbose_name='Untagged VLAN' + ) + tagged_vlans = models.ManyToManyField( + to='ipam.VLAN', + related_name='interfaces_as_tagged', + blank=True, + verbose_name='Tagged VLANs' + ) + tags = TaggableManager(through=TaggedItem) + + csv_headers = [ + 'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', + 'description', 'mode', + ] + + class Meta: + # TODO: ordering and unique_together should include virtual_machine + ordering = ('device', '_name') + unique_together = ('device', 'name') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('dcim:interface', kwargs={'pk': self.pk}) + + def to_csv(self): + return ( + self.device.identifier if self.device else None, + self.virtual_machine.name if self.virtual_machine else None, + self.name, + self.lag.name if self.lag else None, + self.get_type_display(), + self.enabled, + self.mac_address, + self.mtu, + self.mgmt_only, + self.description, + self.get_mode_display(), + ) + + def clean(self): + + # An Interface must belong to a Device *or* to a VirtualMachine + if self.device and self.virtual_machine: + raise ValidationError("An interface cannot belong to both a device and a virtual machine.") + if not self.device and not self.virtual_machine: + raise ValidationError("An interface must belong to either a device or a virtual machine.") + + # VM interfaces must be virtual + if self.virtual_machine and self.type not in VMInterfaceTypeChoices.values(): + raise ValidationError({ + 'type': "Invalid interface type for a virtual machine: {}".format(self.type) + }) + + # Virtual interfaces cannot be connected + if self.type in NONCONNECTABLE_IFACE_TYPES and ( + self.cable or getattr(self, 'circuit_termination', False) + ): + raise ValidationError({ + 'type': "Virtual and wireless interfaces cannot be connected to another interface or circuit. " + "Disconnect the interface or choose a suitable type." + }) + + # An interface's LAG must belong to the same device (or VC master) + if self.lag and self.lag.device not in [self.device, self.device.get_vc_master()]: + raise ValidationError({ + 'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format( + self.lag.name, self.lag.device.name + ) + }) + + # A virtual interface cannot have a parent LAG + if self.type in NONCONNECTABLE_IFACE_TYPES and self.lag is not None: + raise ValidationError({ + 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_type_display()) + }) + + # Only a LAG can have LAG members + if self.type != InterfaceTypeChoices.TYPE_LAG and self.member_interfaces.exists(): + raise ValidationError({ + 'type': "Cannot change interface type; it has LAG members ({}).".format( + ", ".join([iface.name for iface in self.member_interfaces.all()]) + ) + }) + + # Validate untagged VLAN + if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]: + raise ValidationError({ + 'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent " + "device/VM, or it must be global".format(self.untagged_vlan) + }) + + def save(self, *args, **kwargs): + + # Remove untagged VLAN assignment for non-802.1Q interfaces + if self.mode is None: + self.untagged_vlan = None + + # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.) + if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED: + self.tagged_vlans.clear() + + return super().save(*args, **kwargs) + + def to_objectchange(self, action): + # Annotate the parent Device/VM + try: + parent_obj = self.device or self.virtual_machine + except ObjectDoesNotExist: + parent_obj = None + + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + related_object=parent_obj, + object_data=serialize_object(self) + ) + + @property + def connected_endpoint(self): + """ + Return the connected Interface, if it exists, or the connected CircuitTermination, if it exists. We have to + check for ObjectDoesNotExist in case the referenced object has been deleted from the database. + """ + try: + if self._connected_interface: + return self._connected_interface + except ObjectDoesNotExist: + pass + try: + if self._connected_circuittermination: + return self._connected_circuittermination + except ObjectDoesNotExist: + pass + return None + + @connected_endpoint.setter + def connected_endpoint(self, value): + from circuits.models import CircuitTermination + + if value is None: + self._connected_interface = None + self._connected_circuittermination = None + elif isinstance(value, Interface): + self._connected_interface = value + self._connected_circuittermination = None + elif isinstance(value, CircuitTermination): + self._connected_interface = None + self._connected_circuittermination = value + else: + raise ValueError( + "Connected endpoint must be an Interface or CircuitTermination, not {}.".format(type(value)) + ) + + @property + def parent(self): + return self.device or self.virtual_machine + + @property + def is_connectable(self): + return self.type not in NONCONNECTABLE_IFACE_TYPES + + @property + def is_virtual(self): + return self.type in VIRTUAL_IFACE_TYPES + + @property + def is_wireless(self): + return self.type in WIRELESS_IFACE_TYPES + + @property + def is_lag(self): + return self.type == InterfaceTypeChoices.TYPE_LAG + + @property + def count_ipaddresses(self): + return self.ip_addresses.count() + + +# +# Pass-through ports +# + +class FrontPort(CableTermination, ComponentModel): + """ + A pass-through port on the front of a Device. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='frontports' + ) + name = models.CharField( + max_length=64 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=PortTypeChoices + ) + rear_port = models.ForeignKey( + to='dcim.RearPort', + on_delete=models.CASCADE, + related_name='frontports' + ) + rear_port_position = models.PositiveSmallIntegerField( + default=1, + validators=[MinValueValidator(1), MaxValueValidator(64)] + ) + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] + is_path_endpoint = False + + class Meta: + ordering = ('device', '_name') + unique_together = ( + ('device', 'name'), + ('rear_port', 'rear_port_position'), + ) + + def __str__(self): + return self.name + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.get_type_display(), + self.rear_port.name, + self.rear_port_position, + self.description, + ) + + def clean(self): + + # Validate rear port assignment + if self.rear_port.device != self.device: + raise ValidationError( + "Rear port ({}) must belong to the same device".format(self.rear_port) + ) + + # Validate rear port position assignment + if self.rear_port_position > self.rear_port.positions: + raise ValidationError( + "Invalid rear port position ({}); rear port {} has only {} positions".format( + self.rear_port_position, self.rear_port.name, self.rear_port.positions + ) + ) + + +class RearPort(CableTermination, ComponentModel): + """ + A pass-through port on the rear of a Device. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='rearports' + ) + name = models.CharField( + max_length=64 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + type = models.CharField( + max_length=50, + choices=PortTypeChoices + ) + positions = models.PositiveSmallIntegerField( + default=1, + validators=[MinValueValidator(1), MaxValueValidator(64)] + ) + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'positions', 'description'] + is_path_endpoint = False + + class Meta: + ordering = ('device', '_name') + unique_together = ('device', 'name') + + def __str__(self): + return self.name + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.get_type_display(), + self.positions, + self.description, + ) + + +# +# Device bays +# + +class DeviceBay(ComponentModel): + """ + An empty space within a Device which can house a child device + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='device_bays' + ) + name = models.CharField( + max_length=50, + verbose_name='Name' + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + installed_device = models.OneToOneField( + to='dcim.Device', + on_delete=models.SET_NULL, + related_name='parent_bay', + blank=True, + null=True + ) + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'installed_device', 'description'] + + class Meta: + ordering = ('device', '_name') + unique_together = ('device', 'name') + + def __str__(self): + return '{} - {}'.format(self.device.name, self.name) + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.installed_device.identifier if self.installed_device else None, + self.description, + ) + + def clean(self): + + # Validate that the parent Device can have DeviceBays + if not self.device.device_type.is_parent_device: + raise ValidationError("This type of device ({}) does not support device bays.".format( + self.device.device_type + )) + + # Cannot install a device into itself, obviously + if self.device == self.installed_device: + raise ValidationError("Cannot install a device into itself.") + + # Check that the installed device is not already installed elsewhere + if self.installed_device: + current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first() + if current_bay and current_bay != self: + raise ValidationError({ + 'installed_device': "Cannot install the specified device; device is already installed in {}".format( + current_bay + ) + }) + + +# +# Inventory items +# + +class InventoryItem(ComponentModel): + """ + An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. + InventoryItems are used only for inventory purposes. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='inventory_items' + ) + parent = models.ForeignKey( + to='self', + on_delete=models.CASCADE, + related_name='child_items', + blank=True, + null=True + ) + name = models.CharField( + max_length=50, + verbose_name='Name' + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + manufacturer = models.ForeignKey( + to='dcim.Manufacturer', + on_delete=models.PROTECT, + related_name='inventory_items', + blank=True, + null=True + ) + part_id = models.CharField( + max_length=50, + verbose_name='Part ID', + blank=True + ) + serial = models.CharField( + max_length=50, + verbose_name='Serial number', + blank=True + ) + asset_tag = models.CharField( + max_length=50, + unique=True, + blank=True, + null=True, + verbose_name='Asset tag', + help_text='A unique tag used to identify this item' + ) + discovered = models.BooleanField( + default=False, + verbose_name='Discovered' + ) + + tags = TaggableManager(through=TaggedItem) + + csv_headers = [ + 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', + ] + + class Meta: + ordering = ('device__id', 'parent__id', '_name') + unique_together = ('device', 'parent', 'name') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('dcim:device_inventory', kwargs={'pk': self.device.pk}) + + def to_csv(self): + return ( + self.device.name or '{{{}}}'.format(self.device.pk), + self.name, + self.manufacturer.name if self.manufacturer else None, + self.part_id, + self.serial, + self.asset_tag, + self.discovered, + self.description, + ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 9b3b405aa..bc91dd70c 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -41,7 +41,7 @@ DEVICE_LINK = """ """ REGION_ACTIONS = """ - + {% if perms.dcim.change_region %} @@ -50,7 +50,7 @@ REGION_ACTIONS = """ """ RACKGROUP_ACTIONS = """ - + @@ -64,7 +64,7 @@ RACKGROUP_ACTIONS = """ """ RACKROLE_ACTIONS = """ - + {% if perms.dcim.change_rackrole %} @@ -86,7 +86,7 @@ RACK_DEVICE_COUNT = """ """ RACKRESERVATION_ACTIONS = """ - + {% if perms.dcim.change_rackreservation %} @@ -95,7 +95,7 @@ RACKRESERVATION_ACTIONS = """ """ MANUFACTURER_ACTIONS = """ - + {% if perms.dcim.change_manufacturer %} @@ -104,7 +104,7 @@ MANUFACTURER_ACTIONS = """ """ DEVICEROLE_ACTIONS = """ - + {% if perms.dcim.change_devicerole %} @@ -129,7 +129,7 @@ PLATFORM_VM_COUNT = """ """ PLATFORM_ACTIONS = """ - + {% if perms.dcim.change_platform %} @@ -156,10 +156,6 @@ DEVICE_PRIMARY_IP = """ {{ record.primary_ip4.address.ip|default:"" }} """ -SUBDEVICE_ROLE_TEMPLATE = """ -{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %} -""" - DEVICETYPE_INSTANCES_TEMPLATE = """ {{ record.instance_count }} """ @@ -170,7 +166,7 @@ UTILIZATION_GRAPH = """ """ VIRTUALCHASSIS_ACTIONS = """ - + {% if perms.dcim.change_virtualchassis %} @@ -204,6 +200,11 @@ def get_component_template_actions(model_name): {{% endif %}} + {{% if perms.dcim.delete_{model_name} %}} + + + + {{% endif %}} """.format(model_name=model_name).strip() @@ -233,7 +234,7 @@ class RegionTable(BaseTable): class SiteTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) + name = tables.LinkColumn(order_by=('_name',)) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') region = tables.TemplateColumn(template_code=SITE_REGION_LINK) tenant = tables.TemplateColumn(template_code=COL_TENANT) @@ -276,16 +277,17 @@ class RackGroupTable(BaseTable): class RackRoleTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(verbose_name='Name') rack_count = tables.Column(verbose_name='Racks') - color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color') - slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, - verbose_name='') + color = tables.TemplateColumn(COLOR_LABEL) + actions = tables.TemplateColumn( + template_code=RACKROLE_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = RackRole - fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions') + fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions') # @@ -294,7 +296,7 @@ class RackRoleTable(BaseTable): class RackTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) + name = tables.LinkColumn(order_by=('_name',)) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') tenant = tables.TemplateColumn(template_code=COL_TENANT) @@ -393,10 +395,6 @@ class DeviceTypeTable(BaseTable): verbose_name='Device Type' ) is_full_depth = BooleanColumn(verbose_name='Full Depth') - subdevice_role = tables.TemplateColumn( - template_code=SUBDEVICE_ROLE_TEMPLATE, - verbose_name='Subdevice Role' - ) instance_count = tables.TemplateColumn( template_code=DEVICETYPE_INSTANCES_TEMPLATE, verbose_name='Instances' @@ -416,6 +414,7 @@ class DeviceTypeTable(BaseTable): class ConsolePortTemplateTable(BaseTable): pk = ToggleColumn() + name = tables.Column(order_by=('_name',)) actions = tables.TemplateColumn( template_code=get_component_template_actions('consoleporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -424,12 +423,22 @@ class ConsolePortTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = ConsolePortTemplate - fields = ('pk', 'name', 'actions') + fields = ('pk', 'name', 'type', 'actions') empty_text = "None" +class ConsolePortImportTable(BaseTable): + device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + + class Meta(BaseTable.Meta): + model = ConsolePort + fields = ('device', 'name', 'description') + empty_text = False + + class ConsoleServerPortTemplateTable(BaseTable): pk = ToggleColumn() + name = tables.Column(order_by=('_name',)) actions = tables.TemplateColumn( template_code=get_component_template_actions('consoleserverporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -438,12 +447,22 @@ class ConsoleServerPortTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = ConsoleServerPortTemplate - fields = ('pk', 'name', 'actions') + fields = ('pk', 'name', 'type', 'actions') empty_text = "None" +class ConsoleServerPortImportTable(BaseTable): + device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + + class Meta(BaseTable.Meta): + model = ConsoleServerPort + fields = ('device', 'name', 'description') + empty_text = False + + class PowerPortTemplateTable(BaseTable): pk = ToggleColumn() + name = tables.Column(order_by=('_name',)) actions = tables.TemplateColumn( template_code=get_component_template_actions('powerporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -452,12 +471,22 @@ class PowerPortTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = PowerPortTemplate - fields = ('pk', 'name', 'maximum_draw', 'allocated_draw', 'actions') + fields = ('pk', 'name', 'type', 'maximum_draw', 'allocated_draw', 'actions') empty_text = "None" +class PowerPortImportTable(BaseTable): + device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + + class Meta(BaseTable.Meta): + model = PowerPort + fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw') + empty_text = False + + class PowerOutletTemplateTable(BaseTable): pk = ToggleColumn() + name = tables.Column(order_by=('_name',)) actions = tables.TemplateColumn( template_code=get_component_template_actions('poweroutlettemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -466,10 +495,19 @@ class PowerOutletTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = PowerOutletTemplate - fields = ('pk', 'name', 'power_port', 'feed_leg', 'actions') + fields = ('pk', 'name', 'type', 'power_port', 'feed_leg', 'actions') empty_text = "None" +class PowerOutletImportTable(BaseTable): + device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + + class Meta(BaseTable.Meta): + model = PowerOutlet + fields = ('device', 'name', 'description', 'power_port', 'feed_leg') + empty_text = False + + class InterfaceTemplateTable(BaseTable): pk = ToggleColumn() mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}") @@ -485,8 +523,19 @@ class InterfaceTemplateTable(BaseTable): empty_text = "None" +class InterfaceImportTable(BaseTable): + device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine') + + class Meta(BaseTable.Meta): + model = Interface + fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode') + empty_text = False + + class FrontPortTemplateTable(BaseTable): pk = ToggleColumn() + name = tables.Column(order_by=('_name',)) rear_port_position = tables.Column( verbose_name='Position' ) @@ -502,8 +551,18 @@ class FrontPortTemplateTable(BaseTable): empty_text = "None" +class FrontPortImportTable(BaseTable): + device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + + class Meta(BaseTable.Meta): + model = FrontPort + fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position') + empty_text = False + + class RearPortTemplateTable(BaseTable): pk = ToggleColumn() + name = tables.Column(order_by=('_name',)) actions = tables.TemplateColumn( template_code=get_component_template_actions('rearporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -516,8 +575,18 @@ class RearPortTemplateTable(BaseTable): empty_text = "None" +class RearPortImportTable(BaseTable): + device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + + class Meta(BaseTable.Meta): + model = RearPort + fields = ('device', 'name', 'description', 'type', 'position') + empty_text = False + + class DeviceBayTemplateTable(BaseTable): pk = ToggleColumn() + name = tables.Column(order_by=('_name',)) actions = tables.TemplateColumn( template_code=get_component_template_actions('devicebaytemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -558,7 +627,7 @@ class DeviceRoleTable(BaseTable): class Meta(BaseTable.Meta): model = DeviceRole - fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions') + fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions') # @@ -597,7 +666,7 @@ class PlatformTable(BaseTable): class DeviceTable(BaseTable): pk = ToggleColumn() name = tables.TemplateColumn( - order_by=('_nat1', '_nat2', '_nat3'), + order_by=('_name',), template_code=DEVICE_LINK ) status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') @@ -645,32 +714,75 @@ class DeviceImportTable(BaseTable): # Device components # +class DeviceComponentDetailTable(BaseTable): + pk = ToggleColumn() + name = tables.Column(order_by=('_name',)) + cable = tables.LinkColumn() + + class Meta(BaseTable.Meta): + order_by = ('device', 'name') + fields = ('pk', 'device', 'name', 'type', 'description', 'cable') + sequence = ('pk', 'device', 'name', 'type', 'description', 'cable') + + class ConsolePortTable(BaseTable): + name = tables.Column(order_by=('_name',)) class Meta(BaseTable.Meta): model = ConsolePort - fields = ('name',) + fields = ('name', 'type') + + +class ConsolePortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta): + pass class ConsoleServerPortTable(BaseTable): + name = tables.Column(order_by=('_name',)) class Meta(BaseTable.Meta): model = ConsoleServerPort fields = ('name', 'description') +class ConsoleServerPortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta): + pass + + class PowerPortTable(BaseTable): + name = tables.Column(order_by=('_name',)) class Meta(BaseTable.Meta): model = PowerPort - fields = ('name',) + fields = ('name', 'type') + + +class PowerPortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta): + pass class PowerOutletTable(BaseTable): + name = tables.Column(order_by=('_name',)) class Meta(BaseTable.Meta): model = PowerOutlet - fields = ('name', 'description') + fields = ('name', 'type', 'description') + + +class PowerOutletDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta): + pass class InterfaceTable(BaseTable): @@ -680,7 +792,19 @@ class InterfaceTable(BaseTable): fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description') +class InterfaceDetailTable(DeviceComponentDetailTable): + parent = tables.LinkColumn(order_by=('device', 'virtual_machine')) + name = tables.LinkColumn() + enabled = BooleanColumn() + + class Meta(InterfaceTable.Meta): + order_by = ('parent', 'name') + fields = ('pk', 'parent', 'name', 'enabled', 'type', 'description', 'cable') + sequence = ('pk', 'parent', 'name', 'enabled', 'type', 'description', 'cable') + + class FrontPortTable(BaseTable): + name = tables.Column(order_by=('_name',)) class Meta(BaseTable.Meta): model = FrontPort @@ -688,7 +812,15 @@ class FrontPortTable(BaseTable): empty_text = "None" +class FrontPortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta): + pass + + class RearPortTable(BaseTable): + name = tables.Column(order_by=('_name',)) class Meta(BaseTable.Meta): model = RearPort @@ -696,13 +828,41 @@ class RearPortTable(BaseTable): empty_text = "None" +class RearPortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta): + pass + + class DeviceBayTable(BaseTable): + name = tables.Column(order_by=('_name',)) class Meta(BaseTable.Meta): model = DeviceBay fields = ('name',) +class DeviceBayDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + installed_device = tables.LinkColumn() + + class Meta(DeviceBayTable.Meta): + fields = ('pk', 'name', 'device', 'installed_device') + sequence = ('pk', 'name', 'device', 'installed_device') + exclude = ('cable',) + + +class DeviceBayImportTable(BaseTable): + device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device') + + class Meta(BaseTable.Meta): + model = DeviceBay + fields = ('device', 'name', 'installed_device', 'description') + empty_text = False + + # # Cables # diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 14ca704cd..146ef41fc 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,8 +1,11 @@ +from django.contrib.contenttypes.models import ContentType from django.urls import reverse from netaddr import IPNetwork from rest_framework import status from circuits.models import Circuit, CircuitTermination, CircuitType, Provider +from dcim.api import serializers +from dcim.choices import * from dcim.constants import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -11,11 +14,94 @@ from dcim.models import ( Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis, ) from ipam.models import IPAddress, VLAN -from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE -from utilities.testing import APITestCase +from extras.models import Graph +from utilities.testing import APITestCase, choices_to_dict from virtualization.models import Cluster, ClusterType +class AppTest(APITestCase): + + def test_root(self): + + url = reverse('dcim-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('dcim-api:field-choice-list') + response = self.client.get(url, **self.header) + + self.assertEqual(response.status_code, 200) + + # Cable + self.assertEqual(choices_to_dict(response.data.get('cable:length_unit')), CableLengthUnitChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('cable:status')), CableStatusChoices.as_dict()) + content_types = ContentType.objects.filter(CABLE_TERMINATION_MODELS) + cable_termination_choices = { + "{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types + } + self.assertEqual(choices_to_dict(response.data.get('cable:termination_a_type')), cable_termination_choices) + self.assertEqual(choices_to_dict(response.data.get('cable:termination_b_type')), cable_termination_choices) + self.assertEqual(choices_to_dict(response.data.get('cable:type')), CableTypeChoices.as_dict()) + + # Console ports + self.assertEqual(choices_to_dict(response.data.get('console-port:type')), ConsolePortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('console-port:connection_status')), dict(CONNECTION_STATUS_CHOICES)) + self.assertEqual(choices_to_dict(response.data.get('console-port-template:type')), ConsolePortTypeChoices.as_dict()) + + # Console server ports + self.assertEqual(choices_to_dict(response.data.get('console-server-port:type')), ConsolePortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('console-server-port-template:type')), ConsolePortTypeChoices.as_dict()) + + # Device + self.assertEqual(choices_to_dict(response.data.get('device:face')), DeviceFaceChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('device:status')), DeviceStatusChoices.as_dict()) + + # Device type + self.assertEqual(choices_to_dict(response.data.get('device-type:subdevice_role')), SubdeviceRoleChoices.as_dict()) + + # Front ports + self.assertEqual(choices_to_dict(response.data.get('front-port:type')), PortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('front-port-template:type')), PortTypeChoices.as_dict()) + + # Interfaces + self.assertEqual(choices_to_dict(response.data.get('interface:type')), InterfaceTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('interface:mode')), InterfaceModeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('interface-template:type')), InterfaceTypeChoices.as_dict()) + + # Power feed + self.assertEqual(choices_to_dict(response.data.get('power-feed:phase')), PowerFeedPhaseChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-feed:status')), PowerFeedStatusChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-feed:supply')), PowerFeedSupplyChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-feed:type')), PowerFeedTypeChoices.as_dict()) + + # Power outlets + self.assertEqual(choices_to_dict(response.data.get('power-outlet:type')), PowerOutletTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-outlet:feed_leg')), PowerOutletFeedLegChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:type')), PowerOutletTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:feed_leg')), PowerOutletFeedLegChoices.as_dict()) + + # Power ports + self.assertEqual(choices_to_dict(response.data.get('power-port:type')), PowerPortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-port:connection_status')), dict(CONNECTION_STATUS_CHOICES)) + self.assertEqual(choices_to_dict(response.data.get('power-port-template:type')), PowerPortTypeChoices.as_dict()) + + # Rack + self.assertEqual(choices_to_dict(response.data.get('rack:type')), RackTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('rack:width')), RackWidthChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('rack:status')), RackStatusChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('rack:outer_unit')), RackDimensionUnitChoices.as_dict()) + + # Rear ports + self.assertEqual(choices_to_dict(response.data.get('rear-port:type')), PortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('rear-port-template:type')), PortTypeChoices.as_dict()) + + # Site + self.assertEqual(choices_to_dict(response.data.get('site:status')), SiteStatusChoices.as_dict()) + + class RegionTest(APITestCase): def setUp(self): @@ -138,16 +224,20 @@ class SiteTest(APITestCase): def test_get_site_graphs(self): + site_ct = ContentType.objects.get_for_model(Site) self.graph1 = Graph.objects.create( - type=GRAPH_TYPE_SITE, name='Test Graph 1', + type=site_ct, + name='Test Graph 1', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1' ) self.graph2 = Graph.objects.create( - type=GRAPH_TYPE_SITE, name='Test Graph 2', + type=site_ct, + name='Test Graph 2', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2' ) self.graph3 = Graph.objects.create( - type=GRAPH_TYPE_SITE, name='Test Graph 3', + type=site_ct, + name='Test Graph 3', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3' ) @@ -180,7 +270,7 @@ class SiteTest(APITestCase): 'name': 'Test Site 4', 'slug': 'test-site-4', 'region': self.region1.pk, - 'status': SITE_STATUS_ACTIVE, + 'status': SiteStatusChoices.STATUS_ACTIVE, } url = reverse('dcim-api:site-list') @@ -200,19 +290,19 @@ class SiteTest(APITestCase): 'name': 'Test Site 4', 'slug': 'test-site-4', 'region': self.region1.pk, - 'status': SITE_STATUS_ACTIVE, + 'status': SiteStatusChoices.STATUS_ACTIVE, }, { 'name': 'Test Site 5', 'slug': 'test-site-5', 'region': self.region1.pk, - 'status': SITE_STATUS_ACTIVE, + 'status': SiteStatusChoices.STATUS_ACTIVE, }, { 'name': 'Test Site 6', 'slug': 'test-site-6', 'region': self.region1.pk, - 'status': SITE_STATUS_ACTIVE, + 'status': SiteStatusChoices.STATUS_ACTIVE, }, ] @@ -506,6 +596,43 @@ class RackTest(APITestCase): self.assertEqual(response.data['count'], 42) + def test_get_elevation_rack_units(self): + + url = '{}?q=3'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk})) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 13) + + url = '{}?q=U3'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk})) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 11) + + url = '{}?q=10'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk})) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 1) + + url = '{}?q=U20'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk})) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 1) + + def test_get_rack_elevation(self): + + url = reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 42) + + def test_get_rack_elevation_svg(self): + + url = '{}?render=svg'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk})) + response = self.client.get(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.get('Content-Type'), 'image/svg+xml') + def test_list_racks(self): url = reverse('dcim-api:rack-list') @@ -1343,13 +1470,13 @@ class InterfaceTemplateTest(APITestCase): manufacturer=self.manufacturer, model='Test Device Type 1', slug='test-device-type-1' ) self.interfacetemplate1 = InterfaceTemplate.objects.create( - device_type=self.devicetype, name='Test Interface Template 1' + device_type=self.devicetype, name='Test Interface Template 1', type='1000base-t' ) self.interfacetemplate2 = InterfaceTemplate.objects.create( - device_type=self.devicetype, name='Test Interface Template 2' + device_type=self.devicetype, name='Test Interface Template 2', type='1000base-t' ) self.interfacetemplate3 = InterfaceTemplate.objects.create( - device_type=self.devicetype, name='Test Interface Template 3' + device_type=self.devicetype, name='Test Interface Template 3', type='1000base-t' ) def test_get_interfacetemplate(self): @@ -1371,6 +1498,7 @@ class InterfaceTemplateTest(APITestCase): data = { 'device_type': self.devicetype.pk, 'name': 'Test Interface Template 4', + 'type': '1000base-t', } url = reverse('dcim-api:interfacetemplate-list') @@ -1388,14 +1516,17 @@ class InterfaceTemplateTest(APITestCase): { 'device_type': self.devicetype.pk, 'name': 'Test Interface Template 4', + 'type': '1000base-t', }, { 'device_type': self.devicetype.pk, 'name': 'Test Interface Template 5', + 'type': '1000base-t', }, { 'device_type': self.devicetype.pk, 'name': 'Test Interface Template 6', + 'type': '1000base-t', }, ] @@ -1413,6 +1544,7 @@ class InterfaceTemplateTest(APITestCase): data = { 'device_type': self.devicetype.pk, 'name': 'Test Interface Template X', + 'type': '1000base-x-gbic', } url = reverse('dcim-api:interfacetemplate-detail', kwargs={'pk': self.interfacetemplate1.pk}) @@ -1811,6 +1943,31 @@ class DeviceTest(APITestCase): self.assertEqual(response.data['device_role']['id'], self.devicerole1.pk) self.assertEqual(response.data['cluster']['id'], self.cluster1.pk) + def test_get_device_graphs(self): + + device_ct = ContentType.objects.get_for_model(Device) + self.graph1 = Graph.objects.create( + type=device_ct, + name='Test Graph 1', + source='http://example.com/graphs.py?device={{ obj.name }}&foo=1' + ) + self.graph2 = Graph.objects.create( + type=device_ct, + name='Test Graph 2', + source='http://example.com/graphs.py?device={{ obj.name }}&foo=2' + ) + self.graph3 = Graph.objects.create( + type=device_ct, + name='Test Graph 3', + source='http://example.com/graphs.py?device={{ obj.name }}&foo=3' + ) + + url = reverse('dcim-api:device-graphs', kwargs={'pk': self.device1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(len(response.data), 3) + self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?device=Test Device 1&foo=1') + def test_list_devices(self): url = reverse('dcim-api:device-list') @@ -2045,6 +2202,31 @@ class ConsolePortTest(APITestCase): self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertEqual(ConsolePort.objects.count(), 2) + def test_trace_consoleport(self): + + peer_device = Device.objects.create( + site=Site.objects.first(), + device_type=DeviceType.objects.first(), + device_role=DeviceRole.objects.first(), + name='Peer Device' + ) + console_server_port = ConsoleServerPort.objects.create( + device=peer_device, + name='Console Server Port 1' + ) + cable = Cable(termination_a=self.consoleport1, termination_b=console_server_port, label='Cable 1') + cable.save() + + url = reverse('dcim-api:consoleport-trace', kwargs={'pk': self.consoleport1.pk}) + response = self.client.get(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + segment1 = response.data[0] + self.assertEqual(segment1[0]['name'], self.consoleport1.name) + self.assertEqual(segment1[1]['label'], cable.label) + self.assertEqual(segment1[2]['name'], console_server_port.name) + class ConsoleServerPortTest(APITestCase): @@ -2156,6 +2338,31 @@ class ConsoleServerPortTest(APITestCase): self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertEqual(ConsoleServerPort.objects.count(), 2) + def test_trace_consoleserverport(self): + + peer_device = Device.objects.create( + site=Site.objects.first(), + device_type=DeviceType.objects.first(), + device_role=DeviceRole.objects.first(), + name='Peer Device' + ) + console_port = ConsolePort.objects.create( + device=peer_device, + name='Console Port 1' + ) + cable = Cable(termination_a=self.consoleserverport1, termination_b=console_port, label='Cable 1') + cable.save() + + url = reverse('dcim-api:consoleserverport-trace', kwargs={'pk': self.consoleserverport1.pk}) + response = self.client.get(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + segment1 = response.data[0] + self.assertEqual(segment1[0]['name'], self.consoleserverport1.name) + self.assertEqual(segment1[1]['label'], cable.label) + self.assertEqual(segment1[2]['name'], console_port.name) + class PowerPortTest(APITestCase): @@ -2269,6 +2476,31 @@ class PowerPortTest(APITestCase): self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertEqual(PowerPort.objects.count(), 2) + def test_trace_powerport(self): + + peer_device = Device.objects.create( + site=Site.objects.first(), + device_type=DeviceType.objects.first(), + device_role=DeviceRole.objects.first(), + name='Peer Device' + ) + power_outlet = PowerOutlet.objects.create( + device=peer_device, + name='Power Outlet 1' + ) + cable = Cable(termination_a=self.powerport1, termination_b=power_outlet, label='Cable 1') + cable.save() + + url = reverse('dcim-api:powerport-trace', kwargs={'pk': self.powerport1.pk}) + response = self.client.get(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + segment1 = response.data[0] + self.assertEqual(segment1[0]['name'], self.powerport1.name) + self.assertEqual(segment1[1]['label'], cable.label) + self.assertEqual(segment1[2]['name'], power_outlet.name) + class PowerOutletTest(APITestCase): @@ -2380,6 +2612,31 @@ class PowerOutletTest(APITestCase): self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertEqual(PowerOutlet.objects.count(), 2) + def test_trace_poweroutlet(self): + + peer_device = Device.objects.create( + site=Site.objects.first(), + device_type=DeviceType.objects.first(), + device_role=DeviceRole.objects.first(), + name='Peer Device' + ) + power_port = PowerPort.objects.create( + device=peer_device, + name='Power Port 1' + ) + cable = Cable(termination_a=self.poweroutlet1, termination_b=power_port, label='Cable 1') + cable.save() + + url = reverse('dcim-api:poweroutlet-trace', kwargs={'pk': self.poweroutlet1.pk}) + response = self.client.get(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + segment1 = response.data[0] + self.assertEqual(segment1[0]['name'], self.poweroutlet1.name) + self.assertEqual(segment1[1]['label'], cable.label) + self.assertEqual(segment1[2]['name'], power_port.name) + class InterfaceTest(APITestCase): @@ -2398,9 +2655,9 @@ class InterfaceTest(APITestCase): self.device = Device.objects.create( device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site ) - self.interface1 = Interface.objects.create(device=self.device, name='Test Interface 1') - self.interface2 = Interface.objects.create(device=self.device, name='Test Interface 2') - self.interface3 = Interface.objects.create(device=self.device, name='Test Interface 3') + self.interface1 = Interface.objects.create(device=self.device, name='Test Interface 1', type='1000base-t') + self.interface2 = Interface.objects.create(device=self.device, name='Test Interface 2', type='1000base-t') + self.interface3 = Interface.objects.create(device=self.device, name='Test Interface 3', type='1000base-t') self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1) self.vlan2 = VLAN.objects.create(name="Test VLAN 2", vid=2) @@ -2416,16 +2673,20 @@ class InterfaceTest(APITestCase): def test_get_interface_graphs(self): + interface_ct = ContentType.objects.get_for_model(Interface) self.graph1 = Graph.objects.create( - type=GRAPH_TYPE_INTERFACE, name='Test Graph 1', + type=interface_ct, + name='Test Graph 1', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1' ) self.graph2 = Graph.objects.create( - type=GRAPH_TYPE_INTERFACE, name='Test Graph 2', + type=interface_ct, + name='Test Graph 2', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2' ) self.graph3 = Graph.objects.create( - type=GRAPH_TYPE_INTERFACE, name='Test Graph 3', + type=interface_ct, + name='Test Graph 3', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3' ) @@ -2457,6 +2718,7 @@ class InterfaceTest(APITestCase): data = { 'device': self.device.pk, 'name': 'Test Interface 4', + 'type': '1000base-t', } url = reverse('dcim-api:interface-list') @@ -2473,7 +2735,8 @@ class InterfaceTest(APITestCase): data = { 'device': self.device.pk, 'name': 'Test Interface 4', - 'mode': IFACE_MODE_TAGGED, + 'type': '1000base-t', + 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan3.id, 'tagged_vlans': [self.vlan1.id, self.vlan2.id], } @@ -2494,14 +2757,17 @@ class InterfaceTest(APITestCase): { 'device': self.device.pk, 'name': 'Test Interface 4', + 'type': '1000base-t', }, { 'device': self.device.pk, 'name': 'Test Interface 5', + 'type': '1000base-t', }, { 'device': self.device.pk, 'name': 'Test Interface 6', + 'type': '1000base-t', }, ] @@ -2520,21 +2786,24 @@ class InterfaceTest(APITestCase): { 'device': self.device.pk, 'name': 'Test Interface 4', - 'mode': IFACE_MODE_TAGGED, + 'type': '1000base-t', + 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan2.id, 'tagged_vlans': [self.vlan1.id], }, { 'device': self.device.pk, 'name': 'Test Interface 5', - 'mode': IFACE_MODE_TAGGED, + 'type': '1000base-t', + 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan2.id, 'tagged_vlans': [self.vlan1.id], }, { 'device': self.device.pk, 'name': 'Test Interface 6', - 'mode': IFACE_MODE_TAGGED, + 'type': '1000base-t', + 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan2.id, 'tagged_vlans': [self.vlan1.id], }, @@ -2553,12 +2822,13 @@ class InterfaceTest(APITestCase): def test_update_interface(self): lag_interface = Interface.objects.create( - device=self.device, name='Test LAG Interface', type=IFACE_TYPE_LAG + device=self.device, name='Test LAG Interface', type=InterfaceTypeChoices.TYPE_LAG ) data = { 'device': self.device.pk, 'name': 'Test Interface X', + 'type': '1000base-x-gbic', 'lag': lag_interface.pk, } @@ -2580,6 +2850,262 @@ class InterfaceTest(APITestCase): self.assertEqual(Interface.objects.count(), 2) +class FrontPortTest(APITestCase): + + def setUp(self): + + super().setUp() + + site = Site.objects.create(name='Test Site 1', slug='test-site-1') + manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') + devicetype = DeviceType.objects.create( + manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' + ) + devicerole = DeviceRole.objects.create( + name='Test Device Role 1', slug='test-device-role-1', color='ff0000' + ) + self.device = Device.objects.create( + device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site + ) + rear_ports = RearPort.objects.bulk_create(( + RearPort(device=self.device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C), + RearPort(device=self.device, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), + RearPort(device=self.device, name='Rear Port 3', type=PortTypeChoices.TYPE_8P8C), + RearPort(device=self.device, name='Rear Port 4', type=PortTypeChoices.TYPE_8P8C), + RearPort(device=self.device, name='Rear Port 5', type=PortTypeChoices.TYPE_8P8C), + RearPort(device=self.device, name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C), + )) + self.frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]) + self.frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]) + self.frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[2]) + + def test_get_frontport(self): + + url = reverse('dcim-api:frontport-detail', kwargs={'pk': self.frontport1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['name'], self.frontport1.name) + + def test_list_frontports(self): + + url = reverse('dcim-api:frontport-list') + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 3) + + def test_list_frontports_brief(self): + + url = reverse('dcim-api:frontport-list') + response = self.client.get('{}?brief=1'.format(url), **self.header) + + self.assertEqual( + sorted(response.data['results'][0]), + ['cable', 'device', 'id', 'name', 'url'] + ) + + def test_create_frontport(self): + + rear_port = RearPort.objects.get(name='Rear Port 4') + data = { + 'device': self.device.pk, + 'name': 'Front Port 4', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port': rear_port.pk, + 'rear_port_position': 1, + } + + url = reverse('dcim-api:frontport-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(FrontPort.objects.count(), 4) + frontport4 = FrontPort.objects.get(pk=response.data['id']) + self.assertEqual(frontport4.device_id, data['device']) + self.assertEqual(frontport4.name, data['name']) + + def test_create_frontport_bulk(self): + + rear_ports = RearPort.objects.filter(frontports__isnull=True) + data = [ + { + 'device': self.device.pk, + 'name': 'Front Port 4', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port': rear_ports[0].pk, + 'rear_port_position': 1, + }, + { + 'device': self.device.pk, + 'name': 'Front Port 5', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port': rear_ports[1].pk, + 'rear_port_position': 1, + }, + { + 'device': self.device.pk, + 'name': 'Front Port 6', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port': rear_ports[2].pk, + 'rear_port_position': 1, + }, + ] + + url = reverse('dcim-api:frontport-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(FrontPort.objects.count(), 6) + self.assertEqual(response.data[0]['name'], data[0]['name']) + self.assertEqual(response.data[1]['name'], data[1]['name']) + self.assertEqual(response.data[2]['name'], data[2]['name']) + + def test_update_frontport(self): + + rear_port = RearPort.objects.get(name='Rear Port 4') + data = { + 'device': self.device.pk, + 'name': 'Front Port X', + 'type': PortTypeChoices.TYPE_110_PUNCH, + 'rear_port': rear_port.pk, + 'rear_port_position': 1, + } + + url = reverse('dcim-api:frontport-detail', kwargs={'pk': self.frontport1.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(FrontPort.objects.count(), 3) + frontport1 = FrontPort.objects.get(pk=response.data['id']) + self.assertEqual(frontport1.name, data['name']) + self.assertEqual(frontport1.type, data['type']) + self.assertEqual(frontport1.rear_port, rear_port) + + def test_delete_frontport(self): + + url = reverse('dcim-api:frontport-detail', kwargs={'pk': self.frontport1.pk}) + response = self.client.delete(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) + self.assertEqual(FrontPort.objects.count(), 2) + + +class RearPortTest(APITestCase): + + def setUp(self): + + super().setUp() + + site = Site.objects.create(name='Test Site 1', slug='test-site-1') + manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') + devicetype = DeviceType.objects.create( + manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' + ) + devicerole = DeviceRole.objects.create( + name='Test Device Role 1', slug='test-device-role-1', color='ff0000' + ) + self.device = Device.objects.create( + device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site + ) + self.rearport1 = RearPort.objects.create(device=self.device, type=PortTypeChoices.TYPE_8P8C, name='Rear Port 1') + self.rearport3 = RearPort.objects.create(device=self.device, type=PortTypeChoices.TYPE_8P8C, name='Rear Port 2') + self.rearport1 = RearPort.objects.create(device=self.device, type=PortTypeChoices.TYPE_8P8C, name='Rear Port 3') + + def test_get_rearport(self): + + url = reverse('dcim-api:rearport-detail', kwargs={'pk': self.rearport1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['name'], self.rearport1.name) + + def test_list_rearports(self): + + url = reverse('dcim-api:rearport-list') + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 3) + + def test_list_rearports_brief(self): + + url = reverse('dcim-api:rearport-list') + response = self.client.get('{}?brief=1'.format(url), **self.header) + + self.assertEqual( + sorted(response.data['results'][0]), + ['cable', 'device', 'id', 'name', 'url'] + ) + + def test_create_rearport(self): + + data = { + 'device': self.device.pk, + 'name': 'Front Port 4', + 'type': PortTypeChoices.TYPE_8P8C, + } + + url = reverse('dcim-api:rearport-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(RearPort.objects.count(), 4) + rearport4 = RearPort.objects.get(pk=response.data['id']) + self.assertEqual(rearport4.device_id, data['device']) + self.assertEqual(rearport4.name, data['name']) + + def test_create_rearport_bulk(self): + + data = [ + { + 'device': self.device.pk, + 'name': 'Rear Port 4', + 'type': PortTypeChoices.TYPE_8P8C, + }, + { + 'device': self.device.pk, + 'name': 'Rear Port 5', + 'type': PortTypeChoices.TYPE_8P8C, + }, + { + 'device': self.device.pk, + 'name': 'Rear Port 6', + 'type': PortTypeChoices.TYPE_8P8C, + }, + ] + + url = reverse('dcim-api:rearport-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(RearPort.objects.count(), 6) + self.assertEqual(response.data[0]['name'], data[0]['name']) + self.assertEqual(response.data[1]['name'], data[1]['name']) + self.assertEqual(response.data[2]['name'], data[2]['name']) + + def test_update_rearport(self): + + data = { + 'device': self.device.pk, + 'name': 'Front Port X', + 'type': PortTypeChoices.TYPE_110_PUNCH + } + + url = reverse('dcim-api:rearport-detail', kwargs={'pk': self.rearport1.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(RearPort.objects.count(), 3) + rearport1 = RearPort.objects.get(pk=response.data['id']) + self.assertEqual(rearport1.name, data['name']) + self.assertEqual(rearport1.type, data['type']) + + def test_delete_rearport(self): + + url = reverse('dcim-api:rearport-detail', kwargs={'pk': self.rearport1.pk}) + response = self.client.delete(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) + self.assertEqual(RearPort.objects.count(), 2) + + class DeviceBayTest(APITestCase): def setUp(self): @@ -2590,11 +3116,11 @@ class DeviceBayTest(APITestCase): manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') self.devicetype1 = DeviceType.objects.create( manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type', - subdevice_role=SUBDEVICE_ROLE_PARENT + subdevice_role=SubdeviceRoleChoices.ROLE_PARENT ) self.devicetype2 = DeviceType.objects.create( manufacturer=manufacturer, model='Child Device Type', slug='child-device-type', - subdevice_role=SUBDEVICE_ROLE_CHILD + subdevice_role=SubdeviceRoleChoices.ROLE_CHILD ) devicerole = DeviceRole.objects.create( name='Test Device Role 1', slug='test-device-role-1', color='ff0000' @@ -2841,7 +3367,7 @@ class CableTest(APITestCase): ) for device in [self.device1, self.device2]: for i in range(0, 10): - Interface(device=device, type=IFACE_TYPE_1GE_FIXED, name='eth{}'.format(i)).save() + Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth{}'.format(i)).save() self.cable1 = Cable( termination_a=self.device1.interfaces.get(name='eth0'), @@ -2885,7 +3411,7 @@ class CableTest(APITestCase): 'termination_a_id': interface_a.pk, 'termination_b_type': 'dcim.interface', 'termination_b_id': interface_b.pk, - 'status': CONNECTION_STATUS_PLANNED, + 'status': CableStatusChoices.STATUS_PLANNED, 'label': 'Test Cable 4', } @@ -2939,7 +3465,7 @@ class CableTest(APITestCase): data = { 'label': 'Test Cable X', - 'status': CONNECTION_STATUS_CONNECTED, + 'status': CableStatusChoices.STATUS_CONNECTED, } url = reverse('dcim-api:cable-detail', kwargs={'pk': self.cable1.pk}) @@ -3033,16 +3559,16 @@ class ConnectionTest(APITestCase): device=self.device2, name='Test Console Server Port 1' ) rearport1 = RearPort.objects.create( - device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C + device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C ) frontport1 = FrontPort.objects.create( - device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1 + device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 ) rearport2 = RearPort.objects.create( - device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C + device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C ) frontport2 = FrontPort.objects.create( - device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2 + device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 ) url = reverse('dcim-api:cable-list') @@ -3161,16 +3687,16 @@ class ConnectionTest(APITestCase): device=self.device2, name='Test Interface 2' ) rearport1 = RearPort.objects.create( - device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C + device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C ) frontport1 = FrontPort.objects.create( - device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1 + device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 ) rearport2 = RearPort.objects.create( - device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C + device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C ) frontport2 = FrontPort.objects.create( - device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2 + device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 ) url = reverse('dcim-api:cable-list') @@ -3272,16 +3798,16 @@ class ConnectionTest(APITestCase): circuit=circuit, term_side='A', site=self.site, port_speed=10000 ) rearport1 = RearPort.objects.create( - device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C + device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C ) frontport1 = FrontPort.objects.create( - device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1 + device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 ) rearport2 = RearPort.objects.create( - device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C + device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C ) frontport2 = FrontPort.objects.create( - device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2 + device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 ) url = reverse('dcim-api:cable-list') @@ -3410,23 +3936,23 @@ class VirtualChassisTest(APITestCase): device_type=device_type, device_role=device_role, name='StackSwitch9', site=site ) for i in range(0, 13): - Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) # Create two VirtualChassis with three members each self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1') @@ -3678,22 +4204,22 @@ class PowerFeedTest(APITestCase): site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2' ) self.powerfeed1 = PowerFeed.objects.create( - power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=POWERFEED_TYPE_PRIMARY + power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=PowerFeedTypeChoices.TYPE_PRIMARY ) self.powerfeed2 = PowerFeed.objects.create( - power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=POWERFEED_TYPE_REDUNDANT + power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=PowerFeedTypeChoices.TYPE_REDUNDANT ) self.powerfeed3 = PowerFeed.objects.create( - power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=POWERFEED_TYPE_PRIMARY + power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=PowerFeedTypeChoices.TYPE_PRIMARY ) self.powerfeed4 = PowerFeed.objects.create( - power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=POWERFEED_TYPE_REDUNDANT + power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=PowerFeedTypeChoices.TYPE_REDUNDANT ) self.powerfeed5 = PowerFeed.objects.create( - power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=POWERFEED_TYPE_PRIMARY + power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=PowerFeedTypeChoices.TYPE_PRIMARY ) self.powerfeed6 = PowerFeed.objects.create( - power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=POWERFEED_TYPE_REDUNDANT + power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=PowerFeedTypeChoices.TYPE_REDUNDANT ) def test_get_powerfeed(self): @@ -3726,7 +4252,7 @@ class PowerFeedTest(APITestCase): 'name': 'Test Power Feed 4A', 'power_panel': self.powerpanel1.pk, 'rack': self.rack4.pk, - 'type': POWERFEED_TYPE_PRIMARY, + 'type': PowerFeedTypeChoices.TYPE_PRIMARY, } url = reverse('dcim-api:powerfeed-list') @@ -3746,13 +4272,13 @@ class PowerFeedTest(APITestCase): 'name': 'Test Power Feed 4A', 'power_panel': self.powerpanel1.pk, 'rack': self.rack4.pk, - 'type': POWERFEED_TYPE_PRIMARY, + 'type': PowerFeedTypeChoices.TYPE_PRIMARY, }, { 'name': 'Test Power Feed 4B', 'power_panel': self.powerpanel1.pk, 'rack': self.rack4.pk, - 'type': POWERFEED_TYPE_REDUNDANT, + 'type': PowerFeedTypeChoices.TYPE_REDUNDANT, }, ] @@ -3769,7 +4295,7 @@ class PowerFeedTest(APITestCase): data = { 'name': 'Test Power Feed X', 'rack': self.rack4.pk, - 'type': POWERFEED_TYPE_REDUNDANT, + 'type': PowerFeedTypeChoices.TYPE_REDUNDANT, } url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk}) diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py new file mode 100644 index 000000000..83f40fe56 --- /dev/null +++ b/netbox/dcim/tests/test_filters.py @@ -0,0 +1,2609 @@ +from django.contrib.auth.models import User +from django.test import TestCase + +from dcim.choices import * +from dcim.filters import * +from dcim.models import ( + Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, + DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, + InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerPortTemplate, PowerOutlet, + PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, + VirtualChassis, +) +from ipam.models import IPAddress +from tenancy.models import Tenant +from virtualization.models import Cluster, ClusterType + + +class RegionTestCase(TestCase): + queryset = Region.objects.all() + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + + child_regions = ( + Region(name='Region 1A', slug='region-1a', parent=regions[0]), + Region(name='Region 1B', slug='region-1b', parent=regions[0]), + Region(name='Region 2A', slug='region-2a', parent=regions[1]), + Region(name='Region 2B', slug='region-2b', parent=regions[1]), + Region(name='Region 3A', slug='region-3a', parent=regions[2]), + Region(name='Region 3B', slug='region-3b', parent=regions[2]), + ) + for region in child_regions: + region.save() + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Region 1', 'Region 2']} + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['region-1', 'region-2']} + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2) + + def test_parent(self): + parent_regions = Region.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]} + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 4) + params = {'parent': [parent_regions[0].slug, parent_regions[1].slug]} + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 4) + + +class SiteTestCase(TestCase): + queryset = Site.objects.all() + filterset = SiteFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + + sites = ( + Site(name='Site 1', slug='site-1', region=regions[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'), + Site(name='Site 2', slug='site-2', region=regions[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'), + Site(name='Site 3', slug='site-3', region=regions[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'), + ) + Site.objects.bulk_create(sites) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Site 1', 'Site 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['site-1', 'site-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_facility(self): + params = {'facility': ['Facility 1', 'Facility 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_latitude(self): + params = {'latitude': [10, 20]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_longitude(self): + params = {'longitude': [10, 20]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_contact_name(self): + params = {'contact_name': ['Contact 1', 'Contact 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_contact_phone(self): + params = {'contact_phone': ['123-555-0001', '123-555-0002']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_contact_email(self): + params = {'contact_email': ['contact1@example.com', 'contact2@example.com']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_id__in(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id__in': ','.join([str(id) for id in id_list])} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_status(self): + params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]} + 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 RackGroupTestCase(TestCase): + queryset = RackGroup.objects.all() + filterset = RackGroupFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + + sites = ( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + ) + Site.objects.bulk_create(sites) + + rack_groups = ( + RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), + RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), + RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), + ) + RackGroup.objects.bulk_create(rack_groups) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Rack Group 1', 'Rack Group 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['rack-group-1', 'rack-group-2']} + 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) + + 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 RackRoleTestCase(TestCase): + queryset = RackRole.objects.all() + filterset = RackRoleFilterSet + + @classmethod + def setUpTestData(cls): + + rack_roles = ( + RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000'), + RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00'), + RackRole(name='Rack Role 3', slug='rack-role-3', color='0000ff'), + ) + RackRole.objects.bulk_create(rack_roles) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Rack Role 1', 'Rack Role 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['rack-role-1', 'rack-role-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_color(self): + params = {'color': ['ff0000', '00ff00']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class RackTestCase(TestCase): + queryset = Rack.objects.all() + filterset = RackFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + + sites = ( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + ) + Site.objects.bulk_create(sites) + + rack_groups = ( + RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), + RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), + RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), + ) + RackGroup.objects.bulk_create(rack_groups) + + rack_roles = ( + RackRole(name='Rack Role 1', slug='rack-role-1'), + RackRole(name='Rack Role 2', slug='rack-role-2'), + RackRole(name='Rack Role 3', slug='rack-role-3'), + ) + RackRole.objects.bulk_create(rack_roles) + + racks = ( + Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), + Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_19IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), + Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH), + ) + Rack.objects.bulk_create(racks) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Rack 1', 'Rack 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_facility_id(self): + params = {'facility_id': ['rack-1', 'rack-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_asset_tag(self): + params = {'asset_tag': ['1001', '1002']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_type(self): + # TODO: Test for multiple values + params = {'type': RackTypeChoices.TYPE_2POST} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_width(self): + # TODO: Test for multiple values + params = {'width': RackWidthChoices.WIDTH_19IN} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_u_height(self): + params = {'u_height': [42, 43]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_desc_units(self): + params = {'desc_units': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'desc_units': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_outer_width(self): + params = {'outer_width': [100, 200]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_outer_depth(self): + params = {'outer_depth': [100, 200]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_outer_unit(self): + self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 3) + params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_id__in(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id__in': ','.join([str(id) for id in id_list])} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_region(self): + 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) + + def test_group(self): + groups = RackGroup.objects.all()[:2] + params = {'group_id': [groups[0].pk, groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'group': [groups[0].slug, groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_status(self): + params = {'status': [RackStatusChoices.STATUS_ACTIVE, RackStatusChoices.STATUS_PLANNED]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_role(self): + roles = RackRole.objects.all()[:2] + params = {'role_id': [roles[0].pk, roles[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'role': [roles[0].slug, roles[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_serial(self): + params = {'serial': 'ABC'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'serial': 'abc'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class RackReservationTestCase(TestCase): + queryset = RackReservation.objects.all() + filterset = RackReservationFilterSet + + @classmethod + def setUpTestData(cls): + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3'), + ) + Site.objects.bulk_create(sites) + + rack_groups = ( + RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), + RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), + RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), + ) + RackGroup.objects.bulk_create(rack_groups) + + racks = ( + Rack(name='Rack 1', site=sites[0], group=rack_groups[0]), + Rack(name='Rack 2', site=sites[1], group=rack_groups[1]), + Rack(name='Rack 3', site=sites[2], group=rack_groups[2]), + ) + Rack.objects.bulk_create(racks) + + users = ( + User(username='User 1'), + User(username='User 2'), + User(username='User 3'), + ) + User.objects.bulk_create(users) + + reservations = ( + RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0]), + RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1]), + RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2]), + ) + RackReservation.objects.bulk_create(reservations) + + def test_id__in(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id__in': ','.join([str(id) for id in id_list])} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_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_group(self): + groups = RackGroup.objects.all()[:2] + params = {'group_id': [groups[0].pk, groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'group': [groups[0].slug, groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_user(self): + users = User.objects.all()[:2] + params = {'user_id': [users[0].pk, users[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + # TODO: Filtering by username is broken + # params = {'user': [users[0].username, users[1].username]} + # self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class ManufacturerTestCase(TestCase): + queryset = Manufacturer.objects.all() + filterset = ManufacturerFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), + Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), + ) + Manufacturer.objects.bulk_create(manufacturers) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Manufacturer 1', 'Manufacturer 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['manufacturer-1', 'manufacturer-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class DeviceTypeTestCase(TestCase): + queryset = DeviceType.objects.all() + filterset = DeviceTypeFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), + Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), + ) + Manufacturer.objects.bulk_create(manufacturers) + + device_types = ( + DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True), + DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT), + DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD), + ) + DeviceType.objects.bulk_create(device_types) + + # Add component templates for filtering + ConsolePortTemplate.objects.bulk_create(( + ConsolePortTemplate(device_type=device_types[0], name='Console Port 1'), + ConsolePortTemplate(device_type=device_types[1], name='Console Port 2'), + )) + ConsoleServerPortTemplate.objects.bulk_create(( + ConsoleServerPortTemplate(device_type=device_types[0], name='Console Server Port 1'), + ConsoleServerPortTemplate(device_type=device_types[1], name='Console Server Port 2'), + )) + PowerPortTemplate.objects.bulk_create(( + PowerPortTemplate(device_type=device_types[0], name='Power Port 1'), + PowerPortTemplate(device_type=device_types[1], name='Power Port 2'), + )) + PowerOutletTemplate.objects.bulk_create(( + PowerOutletTemplate(device_type=device_types[0], name='Power Outlet 1'), + PowerOutletTemplate(device_type=device_types[1], name='Power Outlet 2'), + )) + InterfaceTemplate.objects.bulk_create(( + InterfaceTemplate(device_type=device_types[0], name='Interface 1'), + InterfaceTemplate(device_type=device_types[1], name='Interface 2'), + )) + rear_ports = ( + RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C), + RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), + ) + RearPortTemplate.objects.bulk_create(rear_ports) + FrontPortTemplate.objects.bulk_create(( + FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), + FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), + )) + DeviceBayTemplate.objects.bulk_create(( + DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'), + DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'), + )) + + def test_model(self): + params = {'model': ['Model 1', 'Model 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['model-1', 'model-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_part_number(self): + params = {'part_number': ['Part Number 1', 'Part Number 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_u_height(self): + params = {'u_height': [1, 2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_is_full_depth(self): + params = {'is_full_depth': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'is_full_depth': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_subdevice_role(self): + params = {'subdevice_role': SubdeviceRoleChoices.ROLE_PARENT} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_id__in(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id__in': ','.join([str(id) for id in id_list])} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_manufacturer(self): + manufacturers = Manufacturer.objects.all()[:2] + params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_console_ports(self): + params = {'console_ports': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'console_ports': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_console_server_ports(self): + params = {'console_server_ports': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'console_server_ports': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_power_ports(self): + params = {'power_ports': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'power_ports': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_power_outlets(self): + params = {'power_outlets': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'power_outlets': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_interfaces(self): + params = {'interfaces': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'interfaces': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_pass_through_ports(self): + params = {'pass_through_ports': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'pass_through_ports': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_device_bays(self): + params = {'device_bays': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device_bays': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class ConsolePortTemplateTestCase(TestCase): + queryset = ConsolePortTemplate.objects.all() + filterset = ConsolePortTemplateFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + + device_types = ( + DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'), + DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'), + DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'), + ) + DeviceType.objects.bulk_create(device_types) + + ConsolePortTemplate.objects.bulk_create(( + ConsolePortTemplate(device_type=device_types[0], name='Console Port 1'), + ConsolePortTemplate(device_type=device_types[1], name='Console Port 2'), + ConsolePortTemplate(device_type=device_types[2], name='Console Port 3'), + )) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Console Port 1', 'Console Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicetype_id(self): + device_types = DeviceType.objects.all()[:2] + params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class ConsoleServerPortTemplateTestCase(TestCase): + queryset = ConsoleServerPortTemplate.objects.all() + filterset = ConsoleServerPortTemplateFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + + device_types = ( + DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'), + DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'), + DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'), + ) + DeviceType.objects.bulk_create(device_types) + + ConsoleServerPortTemplate.objects.bulk_create(( + ConsoleServerPortTemplate(device_type=device_types[0], name='Console Server Port 1'), + ConsoleServerPortTemplate(device_type=device_types[1], name='Console Server Port 2'), + ConsoleServerPortTemplate(device_type=device_types[2], name='Console Server Port 3'), + )) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Console Server Port 1', 'Console Server Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicetype_id(self): + device_types = DeviceType.objects.all()[:2] + params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class PowerPortTemplateTestCase(TestCase): + queryset = PowerPortTemplate.objects.all() + filterset = PowerPortTemplateFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + + device_types = ( + DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'), + DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'), + DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'), + ) + DeviceType.objects.bulk_create(device_types) + + PowerPortTemplate.objects.bulk_create(( + PowerPortTemplate(device_type=device_types[0], name='Power Port 1', maximum_draw=100, allocated_draw=50), + PowerPortTemplate(device_type=device_types[1], name='Power Port 2', maximum_draw=200, allocated_draw=100), + PowerPortTemplate(device_type=device_types[2], name='Power Port 3', maximum_draw=300, allocated_draw=150), + )) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Power Port 1', 'Power Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicetype_id(self): + device_types = DeviceType.objects.all()[:2] + params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_maximum_draw(self): + params = {'maximum_draw': [100, 200]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_allocated_draw(self): + params = {'allocated_draw': [50, 100]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class PowerOutletTemplateTestCase(TestCase): + queryset = PowerOutletTemplate.objects.all() + filterset = PowerOutletTemplateFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + + device_types = ( + DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'), + DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'), + DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'), + ) + DeviceType.objects.bulk_create(device_types) + + PowerOutletTemplate.objects.bulk_create(( + PowerOutletTemplate(device_type=device_types[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A), + PowerOutletTemplate(device_type=device_types[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B), + PowerOutletTemplate(device_type=device_types[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C), + )) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Power Outlet 1', 'Power Outlet 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicetype_id(self): + device_types = DeviceType.objects.all()[:2] + params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_feed_leg(self): + # TODO: Support filtering for multiple values + params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class InterfaceTemplateTestCase(TestCase): + queryset = InterfaceTemplate.objects.all() + filterset = InterfaceTemplateFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + + device_types = ( + DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'), + DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'), + DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'), + ) + DeviceType.objects.bulk_create(device_types) + + InterfaceTemplate.objects.bulk_create(( + InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True), + InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, mgmt_only=False), + InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_SFP, mgmt_only=False), + )) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Interface 1', 'Interface 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicetype_id(self): + device_types = DeviceType.objects.all()[:2] + params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_type(self): + # TODO: Support filtering for multiple values + params = {'type': InterfaceTypeChoices.TYPE_1GE_FIXED} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_mgmt_only(self): + params = {'mgmt_only': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'mgmt_only': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class FrontPortTemplateTestCase(TestCase): + queryset = FrontPortTemplate.objects.all() + filterset = FrontPortTemplateFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + + device_types = ( + DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'), + DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'), + DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'), + ) + DeviceType.objects.bulk_create(device_types) + + rear_ports = ( + RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C), + RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), + RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_8P8C), + ) + RearPortTemplate.objects.bulk_create(rear_ports) + + FrontPortTemplate.objects.bulk_create(( + FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], type=PortTypeChoices.TYPE_110_PUNCH), + FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC), + )) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Front Port 1', 'Front Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicetype_id(self): + device_types = DeviceType.objects.all()[:2] + params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_type(self): + # TODO: Support filtering for multiple values + params = {'type': PortTypeChoices.TYPE_8P8C} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class RearPortTemplateTestCase(TestCase): + queryset = RearPortTemplate.objects.all() + filterset = RearPortTemplateFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + + device_types = ( + DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'), + DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'), + DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'), + ) + DeviceType.objects.bulk_create(device_types) + + RearPortTemplate.objects.bulk_create(( + RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1), + RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2), + RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3), + )) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Rear Port 1', 'Rear Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicetype_id(self): + device_types = DeviceType.objects.all()[:2] + params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_type(self): + # TODO: Support filtering for multiple values + params = {'type': PortTypeChoices.TYPE_8P8C} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_positions(self): + params = {'positions': [1, 2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class DeviceBayTemplateTestCase(TestCase): + queryset = DeviceBayTemplate.objects.all() + filterset = DeviceBayTemplateFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + + device_types = ( + DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'), + DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'), + DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'), + ) + DeviceType.objects.bulk_create(device_types) + + DeviceBayTemplate.objects.bulk_create(( + DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'), + DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'), + DeviceBayTemplate(device_type=device_types[2], name='Device Bay 3'), + )) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Device Bay 1', 'Device Bay 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicetype_id(self): + device_types = DeviceType.objects.all()[:2] + params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class DeviceRoleTestCase(TestCase): + queryset = DeviceRole.objects.all() + filterset = DeviceRoleFilterSet + + @classmethod + def setUpTestData(cls): + + device_roles = ( + DeviceRole(name='Device Role 1', slug='device-role-1', color='ff0000', vm_role=True), + DeviceRole(name='Device Role 2', slug='device-role-2', color='00ff00', vm_role=True), + DeviceRole(name='Device Role 3', slug='device-role-3', color='0000ff', vm_role=False), + ) + DeviceRole.objects.bulk_create(device_roles) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Device Role 1', 'Device Role 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['device-role-1', 'device-role-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_color(self): + params = {'color': ['ff0000', '00ff00']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_vm_role(self): + params = {'vm_role': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'vm_role': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class PlatformTestCase(TestCase): + queryset = Platform.objects.all() + filterset = PlatformFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), + Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), + ) + Manufacturer.objects.bulk_create(manufacturers) + + platforms = ( + Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1'), + Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2'), + Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3'), + ) + Platform.objects.bulk_create(platforms) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Platform 1', 'Platform 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['platform-1', 'platform-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_napalm_driver(self): + params = {'napalm_driver': ['driver-1', 'driver-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_manufacturer(self): + manufacturers = Manufacturer.objects.all()[:2] + params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class DeviceTestCase(TestCase): + queryset = Device.objects.all() + filterset = DeviceFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), + Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), + ) + Manufacturer.objects.bulk_create(manufacturers) + + device_types = ( + DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', is_full_depth=True), + DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', is_full_depth=True), + DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', is_full_depth=False), + ) + DeviceType.objects.bulk_create(device_types) + + device_roles = ( + DeviceRole(name='Device Role 1', slug='device-role-1'), + DeviceRole(name='Device Role 2', slug='device-role-2'), + DeviceRole(name='Device Role 3', slug='device-role-3'), + ) + DeviceRole.objects.bulk_create(device_roles) + + platforms = ( + Platform(name='Platform 1', slug='platform-1'), + Platform(name='Platform 2', slug='platform-2'), + Platform(name='Platform 3', slug='platform-3'), + ) + Platform.objects.bulk_create(platforms) + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + + sites = ( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + ) + Site.objects.bulk_create(sites) + + rack_groups = ( + RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), + RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), + RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), + ) + RackGroup.objects.bulk_create(rack_groups) + + racks = ( + Rack(name='Rack 1', site=sites[0], group=rack_groups[0]), + Rack(name='Rack 2', site=sites[1], group=rack_groups[1]), + Rack(name='Rack 3', site=sites[2], group=rack_groups[2]), + ) + Rack.objects.bulk_create(racks) + + cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + clusters = ( + Cluster(name='Cluster 1', type=cluster_type), + Cluster(name='Cluster 2', type=cluster_type), + Cluster(name='Cluster 3', type=cluster_type), + ) + Cluster.objects.bulk_create(clusters) + + devices = ( + Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}), + Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]), + Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]), + ) + Device.objects.bulk_create(devices) + + # Add components for filtering + ConsolePort.objects.bulk_create(( + ConsolePort(device=devices[0], name='Console Port 1'), + ConsolePort(device=devices[1], name='Console Port 2'), + )) + ConsoleServerPort.objects.bulk_create(( + ConsoleServerPort(device=devices[0], name='Console Server Port 1'), + ConsoleServerPort(device=devices[1], name='Console Server Port 2'), + )) + PowerPort.objects.bulk_create(( + PowerPort(device=devices[0], name='Power Port 1'), + PowerPort(device=devices[1], name='Power Port 2'), + )) + PowerOutlet.objects.bulk_create(( + PowerOutlet(device=devices[0], name='Power Outlet 1'), + PowerOutlet(device=devices[1], name='Power Outlet 2'), + )) + interfaces = ( + Interface(device=devices[0], name='Interface 1', mac_address='00-00-00-00-00-01'), + Interface(device=devices[1], name='Interface 2', mac_address='00-00-00-00-00-02'), + ) + Interface.objects.bulk_create(interfaces) + rear_ports = ( + RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C), + RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), + ) + RearPort.objects.bulk_create(rear_ports) + FrontPort.objects.bulk_create(( + FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), + FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), + )) + DeviceBay.objects.bulk_create(( + DeviceBay(device=devices[0], name='Device Bay 1'), + DeviceBay(device=devices[1], name='Device Bay 2'), + )) + + # Assign primary IPs for filtering + ipaddresses = ( + IPAddress(family=4, address='192.0.2.1/24', interface=interfaces[0]), + IPAddress(family=4, address='192.0.2.2/24', interface=interfaces[1]), + ) + IPAddress.objects.bulk_create(ipaddresses) + Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0]) + Device.objects.filter(pk=devices[1].pk).update(primary_ip4=ipaddresses[1]) + + # VirtualChassis assignment for filtering + virtual_chassis = VirtualChassis.objects.create(master=devices[0]) + Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1) + Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Device 1', 'Device 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_asset_tag(self): + params = {'asset_tag': ['1001', '1002']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_face(self): + params = {'face': DeviceFaceChoices.FACE_FRONT} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_position(self): + params = {'position': [1, 2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_vc_position(self): + params = {'vc_position': [1, 2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_vc_priority(self): + params = {'vc_priority': [1, 2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_id__in(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id__in': ','.join([str(id) for id in id_list])} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_manufacturer(self): + manufacturers = Manufacturer.objects.all()[:2] + params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicetype(self): + device_types = DeviceType.objects.all()[:2] + params = {'device_type_id': [device_types[0].pk, device_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_devicerole(self): + device_roles = DeviceRole.objects.all()[:2] + params = {'role_id': [device_roles[0].pk, device_roles[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'role': [device_roles[0].slug, device_roles[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_platform(self): + platforms = Platform.objects.all()[:2] + params = {'platform_id': [platforms[0].pk, platforms[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'platform': [platforms[0].slug, platforms[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) + + 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_rackgroup(self): + rack_groups = RackGroup.objects.all()[:2] + params = {'rack_group_id': [rack_groups[0].pk, rack_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_rack(self): + racks = Rack.objects.all()[:2] + params = {'rack_id': [racks[0].pk, racks[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cluster(self): + clusters = Cluster.objects.all()[:2] + params = {'cluster_id': [clusters[0].pk, clusters[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_model(self): + params = {'model': ['model-1', 'model-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_status(self): + params = {'status': [DeviceStatusChoices.STATUS_ACTIVE, DeviceStatusChoices.STATUS_STAGED]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_is_full_depth(self): + params = {'is_full_depth': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'is_full_depth': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_mac_address(self): + params = {'mac_address': ['00-00-00-00-00-01', '00-00-00-00-00-02']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_serial(self): + params = {'serial': 'ABC'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'serial': 'abc'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_has_primary_ip(self): + params = {'has_primary_ip': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'has_primary_ip': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_virtual_chassis_id(self): + params = {'virtual_chassis_id': [VirtualChassis.objects.first().pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_virtual_chassis_member(self): + params = {'virtual_chassis_member': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'virtual_chassis_member': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_console_ports(self): + params = {'console_ports': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'console_ports': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_console_server_ports(self): + params = {'console_server_ports': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'console_server_ports': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_power_ports(self): + params = {'power_ports': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'power_ports': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_power_outlets(self): + params = {'power_outlets': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'power_outlets': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_interfaces(self): + params = {'interfaces': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'interfaces': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_pass_through_ports(self): + params = {'pass_through_ports': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'pass_through_ports': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_device_bays(self): + params = {'device_bays': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device_bays': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_local_context_data(self): + params = {'local_context_data': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'local_context_data': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class ConsolePortTestCase(TestCase): + queryset = ConsolePort.objects.all() + filterset = ConsolePortFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + sites = Site.objects.bulk_create(( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + Site(name='Site X', slug='site-x'), + )) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections + ) + Device.objects.bulk_create(devices) + + console_server_ports = ( + ConsoleServerPort(device=devices[3], name='Console Server Port 1'), + ConsoleServerPort(device=devices[3], name='Console Server Port 2'), + ) + ConsoleServerPort.objects.bulk_create(console_server_ports) + + console_ports = ( + ConsolePort(device=devices[0], name='Console Port 1', description='First'), + ConsolePort(device=devices[1], name='Console Port 2', description='Second'), + ConsolePort(device=devices[2], name='Console Port 3', description='Third'), + ) + ConsolePort.objects.bulk_create(console_ports) + + # Cables + Cable(termination_a=console_ports[0], termination_b=console_server_ports[0]).save() + Cable(termination_a=console_ports[1], termination_b=console_server_ports[1]).save() + # Third port is not connected + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Console Port 1', 'Console Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['First', 'Second']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + # TODO: Fix boolean value + def test_connection_status(self): + params = {'connection_status': 'True'} + 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) + + 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_device(self): + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cabled(self): + params = {'cabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class ConsoleServerPortTestCase(TestCase): + queryset = ConsoleServerPort.objects.all() + filterset = ConsoleServerPortFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + sites = Site.objects.bulk_create(( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + Site(name='Site X', slug='site-x'), + )) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections + ) + Device.objects.bulk_create(devices) + + console_ports = ( + ConsolePort(device=devices[3], name='Console Server Port 1'), + ConsolePort(device=devices[3], name='Console Server Port 2'), + ) + ConsolePort.objects.bulk_create(console_ports) + + console_server_ports = ( + ConsoleServerPort(device=devices[0], name='Console Server Port 1', description='First'), + ConsoleServerPort(device=devices[1], name='Console Server Port 2', description='Second'), + ConsoleServerPort(device=devices[2], name='Console Server Port 3', description='Third'), + ) + ConsoleServerPort.objects.bulk_create(console_server_ports) + + # Cables + Cable(termination_a=console_server_ports[0], termination_b=console_ports[0]).save() + Cable(termination_a=console_server_ports[1], termination_b=console_ports[1]).save() + # Third port is not connected + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Console Server Port 1', 'Console Server Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['First', 'Second']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + # TODO: Fix boolean value + def test_connection_status(self): + params = {'connection_status': 'True'} + 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) + + 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_device(self): + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cabled(self): + params = {'cabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class PowerPortTestCase(TestCase): + queryset = PowerPort.objects.all() + filterset = PowerPortFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + sites = Site.objects.bulk_create(( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + Site(name='Site X', slug='site-x'), + )) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections + ) + Device.objects.bulk_create(devices) + + power_outlets = ( + PowerOutlet(device=devices[3], name='Power Outlet 1'), + PowerOutlet(device=devices[3], name='Power Outlet 2'), + ) + PowerOutlet.objects.bulk_create(power_outlets) + + power_ports = ( + PowerPort(device=devices[0], name='Power Port 1', maximum_draw=100, allocated_draw=50, description='First'), + PowerPort(device=devices[1], name='Power Port 2', maximum_draw=200, allocated_draw=100, description='Second'), + PowerPort(device=devices[2], name='Power Port 3', maximum_draw=300, allocated_draw=150, description='Third'), + ) + PowerPort.objects.bulk_create(power_ports) + + # Cables + Cable(termination_a=power_ports[0], termination_b=power_outlets[0]).save() + Cable(termination_a=power_ports[1], termination_b=power_outlets[1]).save() + # Third port is not connected + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Power Port 1', 'Power Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['First', 'Second']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_maximum_draw(self): + params = {'maximum_draw': [100, 200]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_allocated_draw(self): + params = {'allocated_draw': [50, 100]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + # TODO: Fix boolean value + def test_connection_status(self): + params = {'connection_status': 'True'} + 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) + + 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_device(self): + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cabled(self): + params = {'cabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class PowerOutletTestCase(TestCase): + queryset = PowerOutlet.objects.all() + filterset = PowerOutletFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + sites = Site.objects.bulk_create(( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + Site(name='Site X', slug='site-x'), + )) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections + ) + Device.objects.bulk_create(devices) + + power_ports = ( + PowerPort(device=devices[3], name='Power Outlet 1'), + PowerPort(device=devices[3], name='Power Outlet 2'), + ) + PowerPort.objects.bulk_create(power_ports) + + power_outlets = ( + PowerOutlet(device=devices[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'), + PowerOutlet(device=devices[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'), + PowerOutlet(device=devices[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'), + ) + PowerOutlet.objects.bulk_create(power_outlets) + + # Cables + Cable(termination_a=power_outlets[0], termination_b=power_ports[0]).save() + Cable(termination_a=power_outlets[1], termination_b=power_ports[1]).save() + # Third port is not connected + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Power Outlet 1', 'Power Outlet 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['First', 'Second']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_feed_leg(self): + # TODO: Support filtering for multiple values + params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + # TODO: Fix boolean value + def test_connection_status(self): + params = {'connection_status': 'True'} + 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) + + 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_device(self): + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cabled(self): + params = {'cabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class InterfaceTestCase(TestCase): + queryset = Interface.objects.all() + filterset = InterfaceFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + sites = Site.objects.bulk_create(( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + Site(name='Site X', slug='site-x'), + )) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections + ) + Device.objects.bulk_create(devices) + + interfaces = ( + Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'), + Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'), + Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'), + Interface(device=devices[3], name='Interface 4', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), + Interface(device=devices[3], name='Interface 5', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), + Interface(device=devices[3], name='Interface 6', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False), + ) + Interface.objects.bulk_create(interfaces) + + # Cables + Cable(termination_a=interfaces[0], termination_b=interfaces[3]).save() + Cable(termination_a=interfaces[1], termination_b=interfaces[4]).save() + # Third pair is not connected + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:3] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_name(self): + params = {'name': ['Interface 1', 'Interface 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + # TODO: Fix boolean value + def test_connection_status(self): + params = {'connection_status': 'True'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_enabled(self): + params = {'enabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'enabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_mtu(self): + params = {'mtu': [100, 200]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_mgmt_only(self): + params = {'mgmt_only': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'mgmt_only': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_mode(self): + params = {'mode': InterfaceModeChoices.MODE_ACCESS} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_description(self): + params = {'description': ['First', 'Second']} + 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) + + 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_device(self): + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cabled(self): + params = {'cabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'cabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_kind(self): + params = {'kind': 'physical'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) + params = {'kind': 'virtual'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + + def test_mac_address(self): + params = {'mac_address': ['00-00-00-00-00-01', '00-00-00-00-00-02']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_type(self): + params = {'type': [InterfaceTypeChoices.TYPE_1GE_FIXED, InterfaceTypeChoices.TYPE_1GE_GBIC]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class FrontPortTestCase(TestCase): + queryset = FrontPort.objects.all() + filterset = FrontPortFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + sites = Site.objects.bulk_create(( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + Site(name='Site X', slug='site-x'), + )) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections + ) + Device.objects.bulk_create(devices) + + rear_ports = ( + RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[2], name='Rear Port 3', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[3], name='Rear Port 4', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[3], name='Rear Port 5', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[3], name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C, positions=6), + ) + RearPort.objects.bulk_create(rear_ports) + + front_ports = ( + FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'), + FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'), + FrontPort(device=devices[2], name='Front Port 3', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'), + FrontPort(device=devices[3], name='Front Port 4', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1), + FrontPort(device=devices[3], name='Front Port 5', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1), + FrontPort(device=devices[3], name='Front Port 6', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1), + ) + FrontPort.objects.bulk_create(front_ports) + + # Cables + Cable(termination_a=front_ports[0], termination_b=front_ports[3]).save() + Cable(termination_a=front_ports[1], termination_b=front_ports[4]).save() + # Third port is not connected + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Front Port 1', 'Front Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_type(self): + # TODO: Test for multiple values + params = {'type': PortTypeChoices.TYPE_8P8C} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_description(self): + params = {'description': ['First', 'Second']} + 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) + + 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_device(self): + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cabled(self): + params = {'cabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'cabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class RearPortTestCase(TestCase): + queryset = RearPort.objects.all() + filterset = RearPortFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + sites = Site.objects.bulk_create(( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + Site(name='Site X', slug='site-x'), + )) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections + ) + Device.objects.bulk_create(devices) + + rear_ports = ( + RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'), + RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'), + RearPort(device=devices[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'), + RearPort(device=devices[3], name='Rear Port 4', type=PortTypeChoices.TYPE_FC, positions=4), + RearPort(device=devices[3], name='Rear Port 5', type=PortTypeChoices.TYPE_FC, positions=5), + RearPort(device=devices[3], name='Rear Port 6', type=PortTypeChoices.TYPE_FC, positions=6), + ) + RearPort.objects.bulk_create(rear_ports) + + # Cables + Cable(termination_a=rear_ports[0], termination_b=rear_ports[3]).save() + Cable(termination_a=rear_ports[1], termination_b=rear_ports[4]).save() + # Third port is not connected + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Rear Port 1', 'Rear Port 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_type(self): + # TODO: Test for multiple values + params = {'type': PortTypeChoices.TYPE_8P8C} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_positions(self): + params = {'positions': [1, 2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['First', 'Second']} + 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) + + 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_device(self): + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cabled(self): + params = {'cabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'cabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class DeviceBayTestCase(TestCase): + queryset = DeviceBay.objects.all() + filterset = DeviceBayFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + sites = Site.objects.bulk_create(( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + Site(name='Site X', slug='site-x'), + )) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + ) + Device.objects.bulk_create(devices) + + device_bays = ( + DeviceBay(device=devices[0], name='Device Bay 1', description='First'), + DeviceBay(device=devices[1], name='Device Bay 2', description='Second'), + DeviceBay(device=devices[2], name='Device Bay 3', description='Third'), + ) + DeviceBay.objects.bulk_create(device_bays) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Device Bay 1', 'Device Bay 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['First', 'Second']} + 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) + + 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_device(self): + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class InventoryItemTestCase(TestCase): + queryset = InventoryItem.objects.all() + filterset = InventoryItemFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), + Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), + ) + Manufacturer.objects.bulk_create(manufacturers) + + device_type = DeviceType.objects.create(manufacturer=manufacturers[0], model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + + sites = ( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + ) + Site.objects.bulk_create(sites) + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + ) + Device.objects.bulk_create(devices) + + inventory_items = ( + InventoryItem(device=devices[0], manufacturer=manufacturers[0], name='Inventory Item 1', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'), + InventoryItem(device=devices[1], manufacturer=manufacturers[1], name='Inventory Item 2', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'), + InventoryItem(device=devices[2], manufacturer=manufacturers[2], name='Inventory Item 3', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'), + ) + InventoryItem.objects.bulk_create(inventory_items) + + child_inventory_items = ( + InventoryItem(device=devices[0], name='Inventory Item 1A', parent=inventory_items[0]), + InventoryItem(device=devices[1], name='Inventory Item 2A', parent=inventory_items[1]), + InventoryItem(device=devices[2], name='Inventory Item 3A', parent=inventory_items[2]), + ) + InventoryItem.objects.bulk_create(child_inventory_items) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Inventory Item 1', 'Inventory Item 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_part_id(self): + params = {'part_id': ['1001', '1002']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_asset_tag(self): + params = {'asset_tag': ['1001', '1002']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_discovered(self): + # TODO: Fix boolean value + params = {'discovered': True} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'discovered': False} + 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(), 4) + params = {'region': [regions[0].slug, regions[1].slug]} + 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) + + def test_device(self): + # TODO: Allow multiple values + device = Device.objects.first() + params = {'device_id': device.pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'device': device.name} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_parent_id(self): + parent_items = InventoryItem.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [parent_items[0].pk, parent_items[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_manufacturer(self): + manufacturers = Manufacturer.objects.all()[:2] + params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_serial(self): + params = {'serial': 'ABC'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'serial': 'abc'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class VirtualChassisTestCase(TestCase): + queryset = VirtualChassis.objects.all() + filterset = VirtualChassisFilterSet + + @classmethod + def setUpTestData(cls): + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + + sites = ( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + ) + Site.objects.bulk_create(sites) + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], vc_position=1), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], vc_position=2), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], vc_position=1), + Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], vc_position=2), + Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], vc_position=1), + Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], vc_position=2), + ) + Device.objects.bulk_create(devices) + + virtual_chassis = ( + VirtualChassis(master=devices[0], domain='Domain 1'), + VirtualChassis(master=devices[2], domain='Domain 2'), + VirtualChassis(master=devices[4], domain='Domain 3'), + ) + VirtualChassis.objects.bulk_create(virtual_chassis) + + Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis[0]) + Device.objects.filter(pk=devices[3].pk).update(virtual_chassis=virtual_chassis[1]) + Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[2]) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_domain(self): + params = {'domain': ['Domain 1', 'Domain 2']} + 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) + + 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 CableTestCase(TestCase): + queryset = Cable.objects.all() + filterset = CableFilterSet + + @classmethod + def setUpTestData(cls): + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3'), + ) + Site.objects.bulk_create(sites) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + ) + Tenant.objects.bulk_create(tenants) + + racks = ( + Rack(name='Rack 1', site=sites[0]), + Rack(name='Rack 2', site=sites[1]), + Rack(name='Rack 3', site=sites[2]), + ) + Rack.objects.bulk_create(racks) + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') + device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1, tenant=tenants[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2, tenant=tenants[0]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1, tenant=tenants[1]), + Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2), + Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1), + Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2), + ) + Device.objects.bulk_create(devices) + + interfaces = ( + Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 4', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 5', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 6', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[4], name='Interface 9', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[4], name='Interface 10', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[5], name='Interface 11', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[5], name='Interface 12', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + ) + Interface.objects.bulk_create(interfaces) + + # Cables + Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_label(self): + params = {'label': ['Cable 1', 'Cable 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_length(self): + params = {'length': [10, 20]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_length_unit(self): + params = {'length_unit': CableLengthUnitChoices.UNIT_FOOT} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_type(self): + params = {'type': [CableTypeChoices.TYPE_CAT3, CableTypeChoices.TYPE_CAT5E]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_status(self): + params = {'status': [CableStatusChoices.STATUS_CONNECTED]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'status': [CableStatusChoices.STATUS_PLANNED]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_color(self): + params = {'color': ['aa1409', 'f44336']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_device(self): + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_rack(self): + racks = Rack.objects.all()[:2] + params = {'rack_id': [racks[0].pk, racks[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + params = {'rack': [racks[0].name, racks[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + + def test_site(self): + site = Site.objects.all()[:2] + params = {'site_id': [site[0].pk, site[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + params = {'site': [site[0].slug, site[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + + def test_tenant(self): + tenant = Tenant.objects.all()[:2] + params = {'tenant_id': [tenant[0].pk, tenant[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant': [tenant[0].slug, tenant[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + +class PowerPanelTestCase(TestCase): + queryset = PowerPanel.objects.all() + filterset = PowerPanelFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + + sites = ( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + ) + Site.objects.bulk_create(sites) + + rack_groups = ( + RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), + RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), + RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]), + ) + RackGroup.objects.bulk_create(rack_groups) + + power_panels = ( + PowerPanel(name='Power Panel 1', site=sites[0], rack_group=rack_groups[0]), + PowerPanel(name='Power Panel 2', site=sites[1], rack_group=rack_groups[1]), + PowerPanel(name='Power Panel 3', site=sites[2], rack_group=rack_groups[2]), + ) + PowerPanel.objects.bulk_create(power_panels) + + def test_name(self): + params = {'name': ['Power Panel 1', 'Power Panel 2']} + 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) + + 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_rack_group(self): + rack_groups = RackGroup.objects.all()[:2] + params = {'rack_group_id': [rack_groups[0].pk, rack_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class PowerFeedTestCase(TestCase): + queryset = PowerFeed.objects.all() + filterset = PowerFeedFilterSet + + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() + + sites = ( + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[1]), + Site(name='Site 3', slug='site-3', region=regions[2]), + ) + Site.objects.bulk_create(sites) + + racks = ( + Rack(name='Rack 1', site=sites[0]), + Rack(name='Rack 2', site=sites[1]), + Rack(name='Rack 3', site=sites[2]), + ) + Rack.objects.bulk_create(racks) + + power_panels = ( + PowerPanel(name='Power Panel 1', site=sites[0]), + PowerPanel(name='Power Panel 2', site=sites[1]), + PowerPanel(name='Power Panel 3', site=sites[2]), + ) + PowerPanel.objects.bulk_create(power_panels) + + power_feeds = ( + PowerFeed(power_panel=power_panels[0], rack=racks[0], name='Power Feed 1', status=PowerFeedStatusChoices.STATUS_ACTIVE, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=100, amperage=100, max_utilization=10), + PowerFeed(power_panel=power_panels[1], rack=racks[1], name='Power Feed 2', status=PowerFeedStatusChoices.STATUS_FAILED, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=200, amperage=200, max_utilization=20), + PowerFeed(power_panel=power_panels[2], rack=racks[2], name='Power Feed 3', status=PowerFeedStatusChoices.STATUS_OFFLINE, type=PowerFeedTypeChoices.TYPE_REDUNDANT, supply=PowerFeedSupplyChoices.SUPPLY_DC, phase=PowerFeedPhaseChoices.PHASE_SINGLE, voltage=300, amperage=300, max_utilization=30), + ) + PowerFeed.objects.bulk_create(power_feeds) + + def test_name(self): + params = {'name': ['Power Feed 1', 'Power Feed 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_status(self): + # TODO: Test for multiple values + params = {'status': PowerFeedStatusChoices.STATUS_ACTIVE} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_type(self): + params = {'type': PowerFeedTypeChoices.TYPE_PRIMARY} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_supply(self): + params = {'supply': PowerFeedSupplyChoices.SUPPLY_AC} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_phase(self): + params = {'phase': PowerFeedPhaseChoices.PHASE_3PHASE} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_voltage(self): + params = {'voltage': [100, 200]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_amperage(self): + params = {'amperage': [100, 200]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_max_utilization(self): + params = {'max_utilization': [10, 20]} + 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) + + 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_power_panel_id(self): + power_panels = PowerPanel.objects.all()[:2] + params = {'power_panel_id': [power_panels[0].pk, power_panels[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_rack_id(self): + racks = Rack.objects.all()[:2] + params = {'rack_id': [racks[0].pk, racks[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +# TODO: Connection filters diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 2f333ea69..29e741560 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -2,6 +2,7 @@ from django.test import TestCase from dcim.forms import * from dcim.models import * +from virtualization.models import Cluster, ClusterGroup, ClusterType def get_id(model, slug): @@ -10,71 +11,108 @@ def get_id(model, slug): class DeviceTestCase(TestCase): - fixtures = ['dcim', 'ipam'] + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') + rack = Rack.objects.create(name='Rack 1', site=site) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create( + manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1 + ) + device_role = DeviceRole.objects.create( + name='Device Role 1', slug='device-role-1', color='ff0000' + ) + Platform.objects.create(name='Platform 1', slug='platform-1') + Device.objects.create( + name='Device 1', device_type=device_type, device_role=device_role, site=site, rack=rack, position=1 + ) + cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + cluster_group = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1') + Cluster.objects.create(name='Cluster 1', type=cluster_type, group=cluster_group) def test_racked_device(self): - test = DeviceForm(data={ - 'name': 'test', - 'device_role': get_id(DeviceRole, 'leaf-switch'), + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'juniper'), - 'device_type': get_id(DeviceType, 'qfx5100-48s'), - 'site': get_id(Site, 'test1'), - 'rack': '1', - 'face': RACK_FACE_FRONT, - 'position': 41, - 'platform': get_id(Platform, 'juniper-junos'), - 'status': DEVICE_STATUS_ACTIVE, + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': Rack.objects.first().pk, + 'face': DeviceFaceChoices.FACE_FRONT, + 'position': 2, + 'platform': Platform.objects.first().pk, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertTrue(test.is_valid(), test.fields['position'].choices) - self.assertTrue(test.save()) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) def test_racked_device_occupied(self): - test = DeviceForm(data={ + form = DeviceForm(data={ 'name': 'test', - 'device_role': get_id(DeviceRole, 'leaf-switch'), + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'juniper'), - 'device_type': get_id(DeviceType, 'qfx5100-48s'), - 'site': get_id(Site, 'test1'), - 'rack': '1', - 'face': RACK_FACE_FRONT, + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': Rack.objects.first().pk, + 'face': DeviceFaceChoices.FACE_FRONT, 'position': 1, - 'platform': get_id(Platform, 'juniper-junos'), - 'status': DEVICE_STATUS_ACTIVE, + 'platform': Platform.objects.first().pk, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertFalse(test.is_valid()) + self.assertFalse(form.is_valid()) + self.assertIn('position', form.errors) def test_non_racked_device(self): - test = DeviceForm(data={ - 'name': 'test', - 'device_role': get_id(DeviceRole, 'pdu'), + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'servertech'), - 'device_type': get_id(DeviceType, 'cwg-24vym415c9'), - 'site': get_id(Site, 'test1'), - 'rack': '1', + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': None, 'face': None, 'position': None, - 'platform': None, - 'status': DEVICE_STATUS_ACTIVE, + 'platform': Platform.objects.first().pk, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertTrue(test.is_valid()) - self.assertTrue(test.save()) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) - def test_non_racked_device_with_face(self): - test = DeviceForm(data={ - 'name': 'test', - 'device_role': get_id(DeviceRole, 'pdu'), + def test_non_racked_device_with_face_position(self): + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'servertech'), - 'device_type': get_id(DeviceType, 'cwg-24vym415c9'), - 'site': get_id(Site, 'test1'), - 'rack': '1', - 'face': RACK_FACE_REAR, - 'position': None, + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': None, + 'face': DeviceFaceChoices.FACE_REAR, + 'position': 10, 'platform': None, - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertTrue(test.is_valid()) - self.assertTrue(test.save()) + self.assertFalse(form.is_valid()) + self.assertIn('face', form.errors) + self.assertIn('position', form.errors) + + def test_initial_data_population(self): + device_type = DeviceType.objects.first() + cluster = Cluster.objects.first() + test = DeviceForm(initial={ + 'device_type': device_type.pk, + 'device_role': DeviceRole.objects.first().pk, + 'status': DeviceStatusChoices.STATUS_ACTIVE, + 'site': Site.objects.first().pk, + 'cluster': cluster.pk, + }) + + # Check that the initial value for the manufacturer is set automatically when assigning the device type + self.assertEqual(test.initial['manufacturer'], device_type.manufacturer.pk) + + # Check that the initial value for the cluster group is set automatically when assigning the cluster + self.assertEqual(test.initial['cluster_group'], cluster.group.pk) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 2b5bed283..c9d5c4c20 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -1,6 +1,9 @@ +from django.core.exceptions import ValidationError from django.test import TestCase +from dcim.choices import * from dcim.models import * +from tenancy.models import Tenant class RackTestCase(TestCase): @@ -87,7 +90,7 @@ class RackTestCase(TestCase): site=self.site1, rack=rack1, position=43, - face=RACK_FACE_FRONT, + face=DeviceFaceChoices.FACE_FRONT, ) device1.save() @@ -117,7 +120,7 @@ class RackTestCase(TestCase): site=self.site1, rack=self.rack, position=10, - face=RACK_FACE_REAR, + face=DeviceFaceChoices.FACE_REAR, ) device1.save() @@ -125,14 +128,14 @@ class RackTestCase(TestCase): self.assertEqual(list(self.rack.units), list(reversed(range(1, 43)))) # Validate inventory (front face) - rack1_inventory_front = self.rack.get_front_elevation() + rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT) self.assertEqual(rack1_inventory_front[-10]['device'], device1) del(rack1_inventory_front[-10]) for u in rack1_inventory_front: self.assertIsNone(u['device']) # Validate inventory (rear face) - rack1_inventory_rear = self.rack.get_rear_elevation() + rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR) self.assertEqual(rack1_inventory_rear[-10]['device'], device1) del(rack1_inventory_rear[-10]) for u in rack1_inventory_rear: @@ -146,7 +149,7 @@ class RackTestCase(TestCase): site=self.site1, rack=self.rack, position=None, - face=None, + face='', ) self.assertTrue(pdu) @@ -187,20 +190,20 @@ class DeviceTestCase(TestCase): device_type=self.device_type, name='Power Outlet 1', power_port=ppt, - feed_leg=POWERFEED_LEG_A + feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A ).save() InterfaceTemplate( device_type=self.device_type, name='Interface 1', - type=IFACE_TYPE_1GE_FIXED, + type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True ).save() rpt = RearPortTemplate( device_type=self.device_type, name='Rear Port 1', - type=PORT_TYPE_8P8C, + type=PortTypeChoices.TYPE_8P8C, positions=8 ) rpt.save() @@ -208,7 +211,7 @@ class DeviceTestCase(TestCase): FrontPortTemplate( device_type=self.device_type, name='Front Port 1', - type=PORT_TYPE_8P8C, + type=PortTypeChoices.TYPE_8P8C, rear_port=rpt, rear_port_position=2 ).save() @@ -251,27 +254,27 @@ class DeviceTestCase(TestCase): device=d, name='Power Outlet 1', power_port=pp, - feed_leg=POWERFEED_LEG_A + feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A ) Interface.objects.get( device=d, name='Interface 1', - type=IFACE_TYPE_1GE_FIXED, + type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True ) rp = RearPort.objects.get( device=d, name='Rear Port 1', - type=PORT_TYPE_8P8C, + type=PortTypeChoices.TYPE_8P8C, positions=8 ) FrontPort.objects.get( device=d, name='Front Port 1', - type=PORT_TYPE_8P8C, + type=PortTypeChoices.TYPE_8P8C, rear_port=rp, rear_port_position=2 ) @@ -281,6 +284,63 @@ class DeviceTestCase(TestCase): name='Device Bay 1' ) + def test_multiple_unnamed_devices(self): + + device1 = Device( + site=self.site, + device_type=self.device_type, + device_role=self.device_role, + name='' + ) + device1.save() + + device2 = Device( + site=device1.site, + device_type=device1.device_type, + device_role=device1.device_role, + name='' + ) + device2.full_clean() + device2.save() + + self.assertEqual(Device.objects.filter(name='').count(), 2) + + def test_device_duplicate_names(self): + + device1 = Device( + site=self.site, + device_type=self.device_type, + device_role=self.device_role, + name='Test Device 1' + ) + device1.save() + + device2 = Device( + site=device1.site, + device_type=device1.device_type, + device_role=device1.device_role, + name=device1.name + ) + + # Two devices assigned to the same Site and no Tenant should fail validation + with self.assertRaises(ValidationError): + device2.full_clean() + + tenant = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1') + device1.tenant = tenant + device1.save() + device2.tenant = tenant + + # Two devices assigned to the same Site and the same Tenant should fail validation + with self.assertRaises(ValidationError): + device2.full_clean() + + device2.tenant = None + + # Two devices assigned to the same Site and different Tenants should pass validation + device2.full_clean() + device2.save() + class CableTestCase(TestCase): @@ -325,9 +385,12 @@ class CableTestCase(TestCase): def test_cable_deletion(self): """ - When a Cable is deleted, the `cable` field on its termination points must be nullified. + When a Cable is deleted, the `cable` field on its termination points must be nullified. The str() method + should still return the PK of the string even after being nullified. """ self.cable.delete() + self.assertIsNone(self.cable.pk) + self.assertNotEqual(str(self.cable), '#None') interface1 = Interface.objects.get(pk=self.interface1.pk) self.assertIsNone(interface1.cable) interface2 = Interface.objects.get(pk=self.interface2.pk) @@ -379,7 +442,7 @@ class CableTestCase(TestCase): """ A cable cannot terminate to a virtual interface """ - virtual_interface = Interface(device=self.device1, name="V1", type=IFACE_TYPE_VIRTUAL) + virtual_interface = Interface(device=self.device1, name="V1", type=InterfaceTypeChoices.TYPE_VIRTUAL) cable = Cable(termination_a=self.interface2, termination_b=virtual_interface) with self.assertRaises(ValidationError): cable.clean() @@ -388,7 +451,7 @@ class CableTestCase(TestCase): """ A cable cannot terminate to a wireless interface """ - wireless_interface = Interface(device=self.device1, name="W1", type=IFACE_TYPE_80211A) + wireless_interface = Interface(device=self.device1, name="W1", type=InterfaceTypeChoices.TYPE_80211A) cable = Cable(termination_a=self.interface2, termination_b=wireless_interface) with self.assertRaises(ValidationError): cable.clean() @@ -421,16 +484,16 @@ class CablePathTestCase(TestCase): device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site ) self.rear_port1 = RearPort.objects.create( - device=self.panel1, name='Rear Port 1', type=PORT_TYPE_8P8C + device=self.panel1, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C ) self.front_port1 = FrontPort.objects.create( - device=self.panel1, name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=self.rear_port1 + device=self.panel1, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port1 ) self.rear_port2 = RearPort.objects.create( - device=self.panel2, name='Rear Port 2', type=PORT_TYPE_8P8C + device=self.panel2, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C ) self.front_port2 = FrontPort.objects.create( - device=self.panel2, name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=self.rear_port2 + device=self.panel2, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port2 ) def test_path_completion(self): @@ -450,18 +513,22 @@ class CablePathTestCase(TestCase): self.assertIsNone(interface1.connection_status) # Third segment - cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2, status=CONNECTION_STATUS_PLANNED) + cable3 = Cable( + termination_a=self.front_port2, + termination_b=self.interface2, + status=CableStatusChoices.STATUS_PLANNED + ) cable3.save() interface1 = Interface.objects.get(pk=self.interface1.pk) self.assertEqual(interface1.connected_endpoint, self.interface2) - self.assertEqual(interface1.connection_status, CONNECTION_STATUS_PLANNED) + self.assertFalse(interface1.connection_status) # Switch third segment from planned to connected - cable3.status = CONNECTION_STATUS_CONNECTED + cable3.status = CableStatusChoices.STATUS_CONNECTED cable3.save() interface1 = Interface.objects.get(pk=self.interface1.pk) self.assertEqual(interface1.connected_endpoint, self.interface2) - self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED) + self.assertTrue(interface1.connection_status) def test_path_teardown(self): @@ -474,7 +541,7 @@ class CablePathTestCase(TestCase): cable3.save() interface1 = Interface.objects.get(pk=self.interface1.pk) self.assertEqual(interface1.connected_endpoint, self.interface2) - self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED) + self.assertTrue(interface1.connection_status) # Remove a cable cable2.delete() diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 6e34b8ae9..704dedb40 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1,74 +1,123 @@ -import urllib.parse +from decimal import Decimal -from django.test import Client, TestCase +import pytz +import yaml +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django.urls import reverse +from netaddr import EUI -from dcim.constants import CABLE_TYPE_CAT6, IFACE_TYPE_1GE_FIXED -from dcim.models import ( - Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup, - RackReservation, RackRole, Site, Region, VirtualChassis, -) -from utilities.testing import create_test_user +from dcim.choices import * +from dcim.constants import * +from dcim.models import * +from ipam.models import VLAN +from utilities.testing import ViewTestCases -class RegionTestCase(TestCase): +def create_test_device(name): + """ + Convenience method for creating a Device (e.g. for component testing). + """ + site, _ = Site.objects.get_or_create(name='Site 1', slug='site-1') + manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1') + devicetype, _ = DeviceType.objects.get_or_create(model='Device Type 1', manufacturer=manufacturer) + devicerole, _ = DeviceRole.objects.get_or_create(name='Device Role 1', slug='device-role-1') + device = Device.objects.create(name=name, site=site, device_type=devicetype, device_role=devicerole) - def setUp(self): - user = create_test_user(permissions=['dcim.view_region']) - self.client = Client() - self.client.force_login(user) + return device + + +class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase): + model = Region + + @classmethod + def setUpTestData(cls): # Create three Regions - for i in range(1, 4): - Region(name='Region {}'.format(i), slug='region-{}'.format(i)).save() + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + Region(name='Region 3', slug='region-3'), + ) + for region in regions: + region.save() - def test_region_list(self): - - url = reverse('dcim:region_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - -class SiteTestCase(TestCase): - - def setUp(self): - user = create_test_user(permissions=['dcim.view_site']) - self.client = Client() - self.client.force_login(user) - - region = Region(name='Region 1', slug='region-1') - region.save() - - Site.objects.bulk_create([ - Site(name='Site 1', slug='site-1', region=region), - Site(name='Site 2', slug='site-2', region=region), - Site(name='Site 3', slug='site-3', region=region), - ]) - - def test_site_list(self): - - url = reverse('dcim:site_list') - params = { - "region": Region.objects.first().slug, + cls.form_data = { + 'name': 'Region X', + 'slug': 'region-x', + 'parent': regions[2].pk, } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_site(self): - - site = Site.objects.first() - response = self.client.get(site.get_absolute_url()) - self.assertEqual(response.status_code, 200) + cls.csv_data = ( + "name,slug", + "Region 4,region-4", + "Region 5,region-5", + "Region 6,region-6", + ) -class RackGroupTestCase(TestCase): +class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = Site - def setUp(self): - user = create_test_user(permissions=['dcim.view_rackgroup']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): + + regions = ( + Region(name='Region 1', slug='region-1'), + Region(name='Region 2', slug='region-2'), + ) + for region in regions: + region.save() + + Site.objects.bulk_create([ + Site(name='Site 1', slug='site-1', region=regions[0]), + Site(name='Site 2', slug='site-2', region=regions[0]), + Site(name='Site 3', slug='site-3', region=regions[0]), + ]) + + cls.form_data = { + 'name': 'Site X', + 'slug': 'site-x', + 'status': SiteStatusChoices.STATUS_PLANNED, + 'region': regions[1].pk, + 'tenant': None, + 'facility': 'Facility X', + 'asn': 65001, + 'time_zone': pytz.UTC, + 'description': 'Site description', + 'physical_address': '742 Evergreen Terrace, Springfield, USA', + 'shipping_address': '742 Evergreen Terrace, Springfield, USA', + 'latitude': Decimal('35.780000'), + 'longitude': Decimal('-78.642000'), + 'contact_name': 'Hank Hill', + 'contact_phone': '123-555-9999', + 'contact_email': 'hank@stricklandpropane.com', + 'comments': 'Test site', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.csv_data = ( + "name,slug", + "Site 4,site-4", + "Site 5,site-5", + "Site 6,site-6", + ) + + cls.bulk_edit_data = { + 'status': SiteStatusChoices.STATUS_PLANNED, + 'region': regions[1].pk, + 'tenant': None, + 'asn': 65009, + 'time_zone': pytz.timezone('US/Eastern'), + 'description': 'New description', + } + + +class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): + model = RackGroup + + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -79,20 +128,25 @@ class RackGroupTestCase(TestCase): RackGroup(name='Rack Group 3', slug='rack-group-3', site=site), ]) - def test_rackgroup_list(self): + cls.form_data = { + 'name': 'Rack Group X', + 'slug': 'rack-group-x', + 'site': site.pk, + } - url = reverse('dcim:rackgroup_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + cls.csv_data = ( + "site,name,slug", + "Site 1,Rack Group 4,rack-group-4", + "Site 1,Rack Group 5,rack-group-5", + "Site 1,Rack Group 6,rack-group-6", + ) -class RackRoleTestCase(TestCase): +class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase): + model = RackRole - def setUp(self): - user = create_test_user(permissions=['dcim.view_rackrole']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): RackRole.objects.bulk_create([ RackRole(name='Rack Role 1', slug='rack-role-1'), @@ -100,80 +154,144 @@ class RackRoleTestCase(TestCase): RackRole(name='Rack Role 3', slug='rack-role-3'), ]) - def test_rackrole_list(self): + cls.form_data = { + 'name': 'Rack Role X', + 'slug': 'rack-role-x', + 'color': 'c0c0c0', + 'description': 'New role', + } - url = reverse('dcim:rackrole_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + cls.csv_data = ( + "name,slug,color", + "Rack Role 4,rack-role-4,ff0000", + "Rack Role 5,rack-role-5,00ff00", + "Rack Role 6,rack-role-6,0000ff", + ) -class RackReservationTestCase(TestCase): +class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = RackReservation - def setUp(self): - user = create_test_user(permissions=['dcim.view_rackreservation']) - self.client = Client() - self.client.force_login(user) + # Disable inapplicable tests + test_get_object = None + test_create_object = None - site = Site(name='Site 1', slug='site-1') - site.save() + # TODO: Fix URL name for view + test_import_objects = None + + @classmethod + def setUpTestData(cls): + + user2 = User.objects.create_user(username='testuser2') + user3 = User.objects.create_user(username='testuser3') + + site = Site.objects.create(name='Site 1', slug='site-1') rack = Rack(name='Rack 1', site=site) rack.save() RackReservation.objects.bulk_create([ - RackReservation(rack=rack, user=user, units=[1, 2, 3], description='Reservation 1'), - RackReservation(rack=rack, user=user, units=[4, 5, 6], description='Reservation 2'), - RackReservation(rack=rack, user=user, units=[7, 8, 9], description='Reservation 3'), + RackReservation(rack=rack, user=user2, units=[1, 2, 3], description='Reservation 1'), + RackReservation(rack=rack, user=user2, units=[4, 5, 6], description='Reservation 2'), + RackReservation(rack=rack, user=user2, units=[7, 8, 9], description='Reservation 3'), ]) - def test_rackreservation_list(self): - - url = reverse('dcim:rackreservation_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - -class RackTestCase(TestCase): - - def setUp(self): - user = create_test_user(permissions=['dcim.view_rack']) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - Rack.objects.bulk_create([ - Rack(name='Rack 1', site=site), - Rack(name='Rack 2', site=site), - Rack(name='Rack 3', site=site), - ]) - - def test_rack_list(self): - - url = reverse('dcim:rack_list') - params = { - "site": Site.objects.first().slug, + cls.form_data = { + 'rack': rack.pk, + 'units': [10, 11, 12], + 'user': user3.pk, + 'tenant': None, + 'description': 'Rack reservation', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_rack(self): - - rack = Rack.objects.first() - response = self.client.get(rack.get_absolute_url()) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'user': user3.pk, + 'tenant': None, + 'description': 'New description', + } -class ManufacturerTypeTestCase(TestCase): +class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = Rack - def setUp(self): - user = create_test_user(permissions=['dcim.view_manufacturer']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(sites) + + rackgroups = ( + RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), + RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]) + ) + RackGroup.objects.bulk_create(rackgroups) + + rackroles = ( + RackRole(name='Rack Role 1', slug='rack-role-1'), + RackRole(name='Rack Role 2', slug='rack-role-2'), + ) + RackRole.objects.bulk_create(rackroles) + + Rack.objects.bulk_create(( + Rack(name='Rack 1', site=sites[0]), + Rack(name='Rack 2', site=sites[0]), + Rack(name='Rack 3', site=sites[0]), + )) + + cls.form_data = { + 'name': 'Rack X', + 'facility_id': 'Facility X', + 'site': sites[1].pk, + 'group': rackgroups[1].pk, + 'tenant': None, + 'status': RackStatusChoices.STATUS_PLANNED, + 'role': rackroles[1].pk, + 'serial': '123456', + 'asset_tag': 'ABCDEF', + 'type': RackTypeChoices.TYPE_CABINET, + 'width': RackWidthChoices.WIDTH_19IN, + 'u_height': 48, + 'desc_units': False, + 'outer_width': 500, + 'outer_depth': 500, + 'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER, + 'comments': 'Some comments', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.csv_data = ( + "site,name,width,u_height", + "Site 1,Rack 4,19,42", + "Site 1,Rack 5,19,42", + "Site 1,Rack 6,19,42", + ) + + cls.bulk_edit_data = { + 'site': sites[1].pk, + 'group': rackgroups[1].pk, + 'tenant': None, + 'status': RackStatusChoices.STATUS_DEPRECATED, + 'role': rackroles[1].pk, + 'serial': '654321', + 'type': RackTypeChoices.TYPE_4POST, + 'width': RackWidthChoices.WIDTH_23IN, + 'u_height': 49, + 'desc_units': True, + 'outer_width': 30, + 'outer_depth': 30, + 'outer_unit': RackDimensionUnitChoices.UNIT_INCH, + 'comments': 'New comments', + } + + +class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase): + model = Manufacturer + + @classmethod + def setUpTestData(cls): Manufacturer.objects.bulk_create([ Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), @@ -181,53 +299,525 @@ class ManufacturerTypeTestCase(TestCase): Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), ]) - def test_manufacturer_list(self): - - url = reverse('dcim:manufacturer_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - -class DeviceTypeTestCase(TestCase): - - def setUp(self): - user = create_test_user(permissions=['dcim.view_devicetype']) - self.client = Client() - self.client.force_login(user) - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - DeviceType.objects.bulk_create([ - DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer), - DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer), - DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturer), - ]) - - def test_devicetype_list(self): - - url = reverse('dcim:devicetype_list') - params = { - "manufacturer": Manufacturer.objects.first().slug, + cls.form_data = { + 'name': 'Manufacturer X', + 'slug': 'manufacturer-x', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_devicetype(self): - - devicetype = DeviceType.objects.first() - response = self.client.get(devicetype.get_absolute_url()) + cls.csv_data = ( + "name,slug", + "Manufacturer 4,manufacturer-4", + "Manufacturer 5,manufacturer-5", + "Manufacturer 6,manufacturer-6", + ) + + +class DeviceTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = DeviceType + + @classmethod + def setUpTestData(cls): + + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2') + ) + Manufacturer.objects.bulk_create(manufacturers) + + DeviceType.objects.bulk_create([ + DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturers[0]), + DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturers[0]), + DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturers[0]), + ]) + + cls.form_data = { + 'manufacturer': manufacturers[1].pk, + 'model': 'Device Type X', + 'slug': 'device-type-x', + 'part_number': '123ABC', + 'u_height': 2, + 'is_full_depth': True, + 'subdevice_role': '', # CharField + 'comments': 'Some comments', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_edit_data = { + 'manufacturer': manufacturers[1].pk, + 'u_height': 3, + 'is_full_depth': False, + } + + def test_import_objects(self): + """ + Custom import test for YAML-based imports (versus CSV) + """ + IMPORT_DATA = """ +manufacturer: Generic +model: TEST-1000 +slug: test-1000 +u_height: 2 +console-ports: + - name: Console Port 1 + type: de-9 + - name: Console Port 2 + type: de-9 + - name: Console Port 3 + type: de-9 +console-server-ports: + - name: Console Server Port 1 + type: rj-45 + - name: Console Server Port 2 + type: rj-45 + - name: Console Server Port 3 + type: rj-45 +power-ports: + - name: Power Port 1 + type: iec-60320-c14 + - name: Power Port 2 + type: iec-60320-c14 + - name: Power Port 3 + type: iec-60320-c14 +power-outlets: + - name: Power Outlet 1 + type: iec-60320-c13 + power_port: Power Port 1 + feed_leg: A + - name: Power Outlet 2 + type: iec-60320-c13 + power_port: Power Port 1 + feed_leg: A + - name: Power Outlet 3 + type: iec-60320-c13 + power_port: Power Port 1 + feed_leg: A +interfaces: + - name: Interface 1 + type: 1000base-t + mgmt_only: true + - name: Interface 2 + type: 1000base-t + - name: Interface 3 + type: 1000base-t +rear-ports: + - name: Rear Port 1 + type: 8p8c + - name: Rear Port 2 + type: 8p8c + - name: Rear Port 3 + type: 8p8c +front-ports: + - name: Front Port 1 + type: 8p8c + rear_port: Rear Port 1 + - name: Front Port 2 + type: 8p8c + rear_port: Rear Port 2 + - name: Front Port 3 + type: 8p8c + rear_port: Rear Port 3 +device-bays: + - name: Device Bay 1 + - name: Device Bay 2 + - name: Device Bay 3 +""" + + # Create the manufacturer + Manufacturer(name='Generic', slug='generic').save() + + # Add all required permissions to the test user + self.add_permissions( + 'dcim.view_devicetype', + 'dcim.add_devicetype', + 'dcim.add_consoleporttemplate', + 'dcim.add_consoleserverporttemplate', + 'dcim.add_powerporttemplate', + 'dcim.add_poweroutlettemplate', + 'dcim.add_interfacetemplate', + 'dcim.add_frontporttemplate', + 'dcim.add_rearporttemplate', + 'dcim.add_devicebaytemplate', + ) + + form_data = { + 'data': IMPORT_DATA, + 'format': 'yaml' + } + response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True) + self.assertHttpStatus(response, 200) + + dt = DeviceType.objects.get(model='TEST-1000') + + # Verify all of the components were created + self.assertEqual(dt.consoleport_templates.count(), 3) + cp1 = ConsolePortTemplate.objects.first() + self.assertEqual(cp1.name, 'Console Port 1') + self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9) + + self.assertEqual(dt.consoleserverport_templates.count(), 3) + csp1 = ConsoleServerPortTemplate.objects.first() + self.assertEqual(csp1.name, 'Console Server Port 1') + self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45) + + self.assertEqual(dt.powerport_templates.count(), 3) + pp1 = PowerPortTemplate.objects.first() + self.assertEqual(pp1.name, 'Power Port 1') + self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14) + + self.assertEqual(dt.poweroutlet_templates.count(), 3) + po1 = PowerOutletTemplate.objects.first() + self.assertEqual(po1.name, 'Power Outlet 1') + self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13) + self.assertEqual(po1.power_port, pp1) + self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A) + + self.assertEqual(dt.interface_templates.count(), 3) + iface1 = InterfaceTemplate.objects.first() + self.assertEqual(iface1.name, 'Interface 1') + self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED) + self.assertTrue(iface1.mgmt_only) + + self.assertEqual(dt.rearport_templates.count(), 3) + rp1 = RearPortTemplate.objects.first() + self.assertEqual(rp1.name, 'Rear Port 1') + + self.assertEqual(dt.frontport_templates.count(), 3) + fp1 = FrontPortTemplate.objects.first() + self.assertEqual(fp1.name, 'Front Port 1') + self.assertEqual(fp1.rear_port, rp1) + self.assertEqual(fp1.rear_port_position, 1) + + self.assertEqual(dt.device_bay_templates.count(), 3) + db1 = DeviceBayTemplate.objects.first() + self.assertEqual(db1.name, 'Device Bay 1') + + def test_devicetype_export(self): + + url = reverse('dcim:devicetype_list') + self.add_permissions('dcim.view_devicetype') + + response = self.client.get('{}?export'.format(url)) self.assertEqual(response.status_code, 200) + data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader)) + self.assertEqual(len(data), 3) + self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1') + self.assertEqual(data[0]['model'], 'Device Type 1') -class DeviceRoleTestCase(TestCase): +# +# DeviceType components +# - def setUp(self): - user = create_test_user(permissions=['dcim.view_devicerole']) - self.client = Client() - self.client.force_login(user) +class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): + model = ConsolePortTemplate + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + ConsolePortTemplate.objects.bulk_create(( + ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 1'), + ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 2'), + ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Console Port Template X', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Console Port Template [4-6]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + cls.bulk_edit_data = { + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + +class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): + model = ConsoleServerPortTemplate + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + ConsoleServerPortTemplate.objects.bulk_create(( + ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 1'), + ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 2'), + ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Console Server Port Template X', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Console Server Port Template [4-6]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + cls.bulk_edit_data = { + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + + +class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): + model = PowerPortTemplate + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + PowerPortTemplate.objects.bulk_create(( + PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 1'), + PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 2'), + PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Power Port Template X', + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Power Port Template [4-6]', + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + } + + cls.bulk_edit_data = { + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + } + + +class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): + model = PowerOutletTemplate + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') + + PowerOutletTemplate.objects.bulk_create(( + PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 1'), + PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 2'), + PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 3'), + )) + + powerports = ( + PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'), + ) + PowerPortTemplate.objects.bulk_create(powerports) + + cls.form_data = { + 'device_type': devicetype.pk, + 'name': 'Power Outlet Template X', + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[0].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + } + + cls.bulk_create_data = { + 'device_type': devicetype.pk, + 'name_pattern': 'Power Outlet Template [4-6]', + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[0].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + } + + cls.bulk_edit_data = { + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + } + + +class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): + model = InterfaceTemplate + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + InterfaceTemplate.objects.bulk_create(( + InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 1'), + InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 2'), + InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Interface Template X', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mgmt_only': True, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Interface Template [4-6]', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mgmt_only': True, + } + + cls.bulk_edit_data = { + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mgmt_only': True, + } + + +class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): + model = FrontPortTemplate + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') + + rearports = ( + RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 4'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 5'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 6'), + ) + RearPortTemplate.objects.bulk_create(rearports) + + FrontPortTemplate.objects.bulk_create(( + FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', rear_port=rearports[0], rear_port_position=1), + FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', rear_port=rearports[1], rear_port_position=1), + FrontPortTemplate(device_type=devicetype, name='Front Port Template 3', rear_port=rearports[2], rear_port_position=1), + )) + + cls.form_data = { + 'device_type': devicetype.pk, + 'name': 'Front Port X', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port': rearports[3].pk, + 'rear_port_position': 1, + } + + cls.bulk_create_data = { + 'device_type': devicetype.pk, + 'name_pattern': 'Front Port [4-6]', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port_set': [ + '{}:1'.format(rp.pk) for rp in rearports[3:6] + ], + } + + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + } + + +class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): + model = RearPortTemplate + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + RearPortTemplate.objects.bulk_create(( + RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 1'), + RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 2'), + RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Rear Port Template X', + 'type': PortTypeChoices.TYPE_8P8C, + 'positions': 2, + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Rear Port Template [4-6]', + 'type': PortTypeChoices.TYPE_8P8C, + 'positions': 2, + } + + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + } + + +class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): + model = DeviceBayTemplate + + # Disable inapplicable views + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetypes = ( + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + ) + DeviceType.objects.bulk_create(devicetypes) + + DeviceBayTemplate.objects.bulk_create(( + DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 1'), + DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 2'), + DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 3'), + )) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'name': 'Device Bay Template X', + } + + cls.bulk_create_data = { + 'device_type': devicetypes[1].pk, + 'name_pattern': 'Device Bay Template [4-6]', + } + + +class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase): + model = DeviceRole + + @classmethod + def setUpTestData(cls): DeviceRole.objects.bulk_create([ DeviceRole(name='Device Role 1', slug='device-role-1'), @@ -235,99 +825,556 @@ class DeviceRoleTestCase(TestCase): DeviceRole(name='Device Role 3', slug='device-role-3'), ]) - def test_devicerole_list(self): - - url = reverse('dcim:devicerole_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - -class PlatformTestCase(TestCase): - - def setUp(self): - user = create_test_user(permissions=['dcim.view_platform']) - self.client = Client() - self.client.force_login(user) - - Platform.objects.bulk_create([ - Platform(name='Platform 1', slug='platform-1'), - Platform(name='Platform 2', slug='platform-2'), - Platform(name='Platform 3', slug='platform-3'), - ]) - - def test_platform_list(self): - - url = reverse('dcim:platform_list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - -class DeviceTestCase(TestCase): - - def setUp(self): - user = create_test_user(permissions=['dcim.view_device']) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - Device.objects.bulk_create([ - Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole), - Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole), - Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole), - ]) - - def test_device_list(self): - - url = reverse('dcim:device_list') - params = { - "device_type_id": DeviceType.objects.first().pk, - "role": DeviceRole.objects.first().slug, + cls.form_data = { + 'name': 'Devie Role X', + 'slug': 'device-role-x', + 'color': 'c0c0c0', + 'vm_role': False, + 'description': 'New device role', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - def test_device(self): - - device = Device.objects.first() - response = self.client.get(device.get_absolute_url()) - self.assertEqual(response.status_code, 200) + cls.csv_data = ( + "name,slug,color", + "Device Role 4,device-role-4,ff0000", + "Device Role 5,device-role-5,00ff00", + "Device Role 6,device-role-6,0000ff", + ) -class InventoryItemTestCase(TestCase): +class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase): + model = Platform - def setUp(self): - user = create_test_user(permissions=['dcim.view_inventoryitem']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): - site = Site(name='Site 1', slug='site-1') - site.save() + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() + Platform.objects.bulk_create([ + Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturer), + Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturer), + Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer), + ]) - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() + cls.form_data = { + 'name': 'Platform X', + 'slug': 'platform-x', + 'manufacturer': manufacturer.pk, + 'napalm_driver': 'junos', + 'napalm_args': None, + } - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() + cls.csv_data = ( + "name,slug", + "Platform 4,platform-4", + "Platform 5,platform-5", + "Platform 6,platform-6", + ) - device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device.save() + +class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = Device + + @classmethod + def setUpTestData(cls): + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(sites) + + racks = ( + Rack(name='Rack 1', site=sites[0]), + Rack(name='Rack 2', site=sites[1]), + ) + Rack.objects.bulk_create(racks) + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + + devicetypes = ( + DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer), + DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer), + ) + DeviceType.objects.bulk_create(devicetypes) + + deviceroles = ( + DeviceRole(name='Device Role 1', slug='device-role-1'), + DeviceRole(name='Device Role 2', slug='device-role-2'), + ) + DeviceRole.objects.bulk_create(deviceroles) + + platforms = ( + Platform(name='Platform 1', slug='platform-1'), + Platform(name='Platform 2', slug='platform-2'), + ) + Platform.objects.bulk_create(platforms) + + Device.objects.bulk_create([ + Device(name='Device 1', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]), + Device(name='Device 2', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]), + Device(name='Device 3', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]), + ]) + + cls.form_data = { + 'device_type': devicetypes[1].pk, + 'device_role': deviceroles[1].pk, + 'tenant': None, + 'platform': platforms[1].pk, + 'name': 'Device X', + 'serial': '123456', + 'asset_tag': 'ABCDEF', + 'site': sites[1].pk, + 'rack': racks[1].pk, + 'position': 1, + 'face': DeviceFaceChoices.FACE_FRONT, + 'status': DeviceStatusChoices.STATUS_PLANNED, + 'primary_ip4': None, + 'primary_ip6': None, + 'cluster': None, + 'virtual_chassis': None, + 'vc_position': None, + 'vc_priority': None, + 'comments': 'A new device', + 'tags': 'Alpha,Bravo,Charlie', + 'local_context_data': None, + } + + cls.csv_data = ( + "device_role,manufacturer,model_name,status,site,name", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 4", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 5", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6", + ) + + cls.bulk_edit_data = { + 'device_type': devicetypes[1].pk, + 'device_role': deviceroles[1].pk, + 'tenant': None, + 'platform': platforms[1].pk, + 'serial': '123456', + 'status': DeviceStatusChoices.STATUS_DECOMMISSIONING, + } + + +class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): + model = ConsolePort + + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + + ConsolePort.objects.bulk_create([ + ConsolePort(device=device, name='Console Port 1'), + ConsolePort(device=device, name='Console Port 2'), + ConsolePort(device=device, name='Console Port 3'), + ]) + + cls.form_data = { + 'device': device.pk, + 'name': 'Console Port X', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'A console port', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Console Port [4-6]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'A console port', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_edit_data = { + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'New description', + } + + cls.csv_data = ( + "device,name", + "Device 1,Console Port 4", + "Device 1,Console Port 5", + "Device 1,Console Port 6", + ) + + +class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): + model = ConsoleServerPort + + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + + ConsoleServerPort.objects.bulk_create([ + ConsoleServerPort(device=device, name='Console Server Port 1'), + ConsoleServerPort(device=device, name='Console Server Port 2'), + ConsoleServerPort(device=device, name='Console Server Port 3'), + ]) + + cls.form_data = { + 'device': device.pk, + 'name': 'Console Server Port X', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'A console server port', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Console Server Port [4-6]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'A console server port', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_edit_data = { + 'device': device.pk, + 'type': ConsolePortTypeChoices.TYPE_RJ45, + 'description': 'New description', + } + + cls.csv_data = ( + "device,name", + "Device 1,Console Server Port 4", + "Device 1,Console Server Port 5", + "Device 1,Console Server Port 6", + ) + + +class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): + model = PowerPort + + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + + PowerPort.objects.bulk_create([ + PowerPort(device=device, name='Power Port 1'), + PowerPort(device=device, name='Power Port 2'), + PowerPort(device=device, name='Power Port 3'), + ]) + + cls.form_data = { + 'device': device.pk, + 'name': 'Power Port X', + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + 'description': 'A power port', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Power Port [4-6]]', + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + 'description': 'A power port', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_edit_data = { + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, + 'allocated_draw': 50, + 'description': 'New description', + } + + cls.csv_data = ( + "device,name", + "Device 1,Power Port 4", + "Device 1,Power Port 5", + "Device 1,Power Port 6", + ) + + +class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): + model = PowerOutlet + + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + + powerports = ( + PowerPort(device=device, name='Power Port 1'), + PowerPort(device=device, name='Power Port 2'), + ) + PowerPort.objects.bulk_create(powerports) + + PowerOutlet.objects.bulk_create([ + PowerOutlet(device=device, name='Power Outlet 1', power_port=powerports[0]), + PowerOutlet(device=device, name='Power Outlet 2', power_port=powerports[0]), + PowerOutlet(device=device, name='Power Outlet 3', power_port=powerports[0]), + ]) + + cls.form_data = { + 'device': device.pk, + 'name': 'Power Outlet X', + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[1].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + 'description': 'A power outlet', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Power Outlet [4-6]', + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[1].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + 'description': 'A power outlet', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_edit_data = { + 'device': device.pk, + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'power_port': powerports[1].pk, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + 'description': 'New description', + } + + cls.csv_data = ( + "device,name", + "Device 1,Power Outlet 4", + "Device 1,Power Outlet 5", + "Device 1,Power Outlet 6", + ) + + +class InterfaceTestCase( + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.DeviceComponentViewTestCase, +): + model = Interface + + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + + interfaces = ( + Interface(device=device, name='Interface 1'), + Interface(device=device, name='Interface 2'), + Interface(device=device, name='Interface 3'), + Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG), + ) + Interface.objects.bulk_create(interfaces) + + vlans = ( + VLAN(vid=1, name='VLAN1', site=device.site), + VLAN(vid=101, name='VLAN101', site=device.site), + VLAN(vid=102, name='VLAN102', site=device.site), + VLAN(vid=103, name='VLAN103', site=device.site), + ) + VLAN.objects.bulk_create(vlans) + + cls.form_data = { + 'device': device.pk, + 'virtual_machine': None, + 'name': 'Interface X', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'enabled': False, + 'lag': interfaces[3].pk, + 'mac_address': EUI('01:02:03:04:05:06'), + 'mtu': 2000, + 'mgmt_only': True, + 'description': 'A front port', + 'mode': InterfaceModeChoices.MODE_TAGGED, + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [v.pk for v in vlans[1:4]], + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Interface [4-6]', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'enabled': False, + 'lag': interfaces[3].pk, + 'mac_address': EUI('01:02:03:04:05:06'), + 'mtu': 2000, + 'mgmt_only': True, + 'description': 'A front port', + 'mode': InterfaceModeChoices.MODE_TAGGED, + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [v.pk for v in vlans[1:4]], + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_edit_data = { + 'device': device.pk, + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'enabled': False, + 'lag': interfaces[3].pk, + 'mac_address': EUI('01:02:03:04:05:06'), + 'mtu': 2000, + 'mgmt_only': True, + 'description': 'New description', + 'mode': InterfaceModeChoices.MODE_TAGGED, + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [v.pk for v in vlans[1:4]], + } + + cls.csv_data = ( + "device,name,type", + "Device 1,Interface 4,1000BASE-T (1GE)", + "Device 1,Interface 5,1000BASE-T (1GE)", + "Device 1,Interface 6,1000BASE-T (1GE)", + ) + + +class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): + model = FrontPort + + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + + rearports = ( + RearPort(device=device, name='Rear Port 1'), + RearPort(device=device, name='Rear Port 2'), + RearPort(device=device, name='Rear Port 3'), + RearPort(device=device, name='Rear Port 4'), + RearPort(device=device, name='Rear Port 5'), + RearPort(device=device, name='Rear Port 6'), + ) + RearPort.objects.bulk_create(rearports) + + FrontPort.objects.bulk_create([ + FrontPort(device=device, name='Front Port 1', rear_port=rearports[0]), + FrontPort(device=device, name='Front Port 2', rear_port=rearports[1]), + FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]), + ]) + + cls.form_data = { + 'device': device.pk, + 'name': 'Front Port X', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port': rearports[3].pk, + 'rear_port_position': 1, + 'description': 'New description', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Front Port [4-6]', + 'type': PortTypeChoices.TYPE_8P8C, + 'rear_port_set': [ + '{}:1'.format(rp.pk) for rp in rearports[3:6] + ], + 'description': 'New description', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + 'description': 'New description', + } + + cls.csv_data = ( + "device,name,type,rear_port,rear_port_position", + "Device 1,Front Port 4,8P8C,Rear Port 4,1", + "Device 1,Front Port 5,8P8C,Rear Port 5,1", + "Device 1,Front Port 6,8P8C,Rear Port 6,1", + ) + + +class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): + model = RearPort + + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + + RearPort.objects.bulk_create([ + RearPort(device=device, name='Rear Port 1'), + RearPort(device=device, name='Rear Port 2'), + RearPort(device=device, name='Rear Port 3'), + ]) + + cls.form_data = { + 'device': device.pk, + 'name': 'Rear Port X', + 'type': PortTypeChoices.TYPE_8P8C, + 'positions': 3, + 'description': 'A rear port', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Rear Port [4-6]', + 'type': PortTypeChoices.TYPE_8P8C, + 'positions': 3, + 'description': 'A rear port', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + 'description': 'New description', + } + + cls.csv_data = ( + "device,name,type,positions", + "Device 1,Rear Port 4,8P8C,1", + "Device 1,Rear Port 5,8P8C,1", + "Device 1,Rear Port 6,8P8C,1", + ) + + +class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase): + model = DeviceBay + + # Disable inapplicable views + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): + device1 = create_test_device('Device 1') + device2 = create_test_device('Device 2') + + # Update the DeviceType subdevice role to allow adding DeviceBays + DeviceType.objects.update(subdevice_role=SubdeviceRoleChoices.ROLE_PARENT) + + DeviceBay.objects.bulk_create([ + DeviceBay(device=device1, name='Device Bay 1'), + DeviceBay(device=device1, name='Device Bay 2'), + DeviceBay(device=device1, name='Device Bay 3'), + ]) + + cls.form_data = { + 'device': device2.pk, + 'name': 'Device Bay X', + 'description': 'A device bay', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.bulk_create_data = { + 'device': device2.pk, + 'name_pattern': 'Device Bay [4-6]', + 'description': 'A device bay', + 'tags': 'Alpha,Bravo,Charlie', + } + + cls.csv_data = ( + "device,name", + "Device 1,Device Bay 4", + "Device 1,Device Bay 5", + "Device 1,Device Bay 6", + ) + + +class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): + model = InventoryItem + + @classmethod + def setUpTestData(cls): + device = create_test_device('Device 1') + manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1') InventoryItem.objects.bulk_create([ InventoryItem(device=device, name='Inventory Item 1'), @@ -335,81 +1382,135 @@ class InventoryItemTestCase(TestCase): InventoryItem(device=device, name='Inventory Item 3'), ]) - def test_inventoryitem_list(self): - - url = reverse('dcim:inventoryitem_list') - params = { - "device_id": Device.objects.first().pk, + cls.form_data = { + 'device': device.pk, + 'manufacturer': manufacturer.pk, + 'name': 'Inventory Item X', + 'parent': None, + 'discovered': False, + 'part_id': '123456', + 'serial': '123ABC', + 'asset_tag': 'ABC123', + 'description': 'An inventory item', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) - - -class CableTestCase(TestCase): - - def setUp(self): - user = create_test_user(permissions=['dcim.view_cable']) - self.client = Client() - self.client.force_login(user) - - site = Site(name='Site 1', slug='site-1') - site.save() - - manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') - manufacturer.save() - - devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) - devicetype.save() - - devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') - devicerole.save() - - device1 = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) - device1.save() - device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole) - device2.save() - - iface1 = Interface(device=device1, name='Interface 1', type=IFACE_TYPE_1GE_FIXED) - iface1.save() - iface2 = Interface(device=device1, name='Interface 2', type=IFACE_TYPE_1GE_FIXED) - iface2.save() - iface3 = Interface(device=device1, name='Interface 3', type=IFACE_TYPE_1GE_FIXED) - iface3.save() - iface4 = Interface(device=device2, name='Interface 1', type=IFACE_TYPE_1GE_FIXED) - iface4.save() - iface5 = Interface(device=device2, name='Interface 2', type=IFACE_TYPE_1GE_FIXED) - iface5.save() - iface6 = Interface(device=device2, name='Interface 3', type=IFACE_TYPE_1GE_FIXED) - iface6.save() - - Cable(termination_a=iface1, termination_b=iface4, type=CABLE_TYPE_CAT6).save() - Cable(termination_a=iface2, termination_b=iface5, type=CABLE_TYPE_CAT6).save() - Cable(termination_a=iface3, termination_b=iface6, type=CABLE_TYPE_CAT6).save() - - def test_cable_list(self): - - url = reverse('dcim:cable_list') - params = { - "type": CABLE_TYPE_CAT6, + cls.bulk_create_data = { + 'device': device.pk, + 'name_pattern': 'Inventory Item [4-6]', + 'manufacturer': manufacturer.pk, + 'parent': None, + 'discovered': False, + 'part_id': '123456', + 'serial': '123ABC', + 'description': 'An inventory item', + 'tags': 'Alpha,Bravo,Charlie', } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + cls.bulk_edit_data = { + 'device': device.pk, + 'manufacturer': manufacturer.pk, + 'part_id': '123456', + 'description': 'New description', + } - def test_cable(self): - - cable = Cable.objects.first() - response = self.client.get(cable.get_absolute_url()) - self.assertEqual(response.status_code, 200) + cls.csv_data = ( + "device,name", + "Device 1,Inventory Item 4", + "Device 1,Inventory Item 5", + "Device 1,Inventory Item 6", + ) -class VirtualChassisTestCase(TestCase): +class CableTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = Cable - def setUp(self): - user = create_test_user(permissions=['dcim.view_virtualchassis']) - self.client = Client() - self.client.force_login(user) + # TODO: Creation URL needs termination context + test_create_object = None + + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer) + devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + + devices = ( + Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole), + Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole), + Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole), + Device(name='Device 4', site=site, device_type=devicetype, device_role=devicerole), + ) + Device.objects.bulk_create(devices) + + interfaces = ( + Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[0], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + ) + Interface.objects.bulk_create(interfaces) + + Cable(termination_a=interfaces[0], termination_b=interfaces[3], type=CableTypeChoices.TYPE_CAT6).save() + Cable(termination_a=interfaces[1], termination_b=interfaces[4], type=CableTypeChoices.TYPE_CAT6).save() + Cable(termination_a=interfaces[2], termination_b=interfaces[5], type=CableTypeChoices.TYPE_CAT6).save() + + interface_ct = ContentType.objects.get_for_model(Interface) + cls.form_data = { + # Changing terminations not supported when editing an existing Cable + 'termination_a_type': interface_ct.pk, + 'termination_a_id': interfaces[0].pk, + 'termination_b_type': interface_ct.pk, + 'termination_b_id': interfaces[3].pk, + 'type': CableTypeChoices.TYPE_CAT6, + 'status': CableStatusChoices.STATUS_PLANNED, + 'label': 'Label', + 'color': 'c0c0c0', + 'length': 100, + 'length_unit': CableLengthUnitChoices.UNIT_FOOT, + } + + cls.csv_data = ( + "side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name", + "Device 3,interface,Interface 1,Device 4,interface,Interface 1", + "Device 3,interface,Interface 2,Device 4,interface,Interface 2", + "Device 3,interface,Interface 3,Device 4,interface,Interface 3", + ) + + cls.bulk_edit_data = { + 'type': CableTypeChoices.TYPE_CAT5E, + 'status': CableStatusChoices.STATUS_CONNECTED, + 'label': 'New label', + 'color': '00ff00', + 'length': 50, + 'length_unit': CableLengthUnitChoices.UNIT_METER, + } + + +class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = VirtualChassis + + # Disable inapplicable tests + test_get_object = None + test_import_objects = None + test_bulk_edit_objects = None + test_bulk_delete_objects = None + + # TODO: Requires special form handling + test_create_object = None + test_edit_object = None + + @classmethod + def setUpTestData(cls): site = Site.objects.create(name='Site 1', slug='site-1') manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1') @@ -448,9 +1549,110 @@ class VirtualChassisTestCase(TestCase): vc3 = VirtualChassis.objects.create(master=device5, domain='test-domain-3') Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2) - def test_virtualchassis_list(self): - url = reverse('dcim:virtualchassis_list') +class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = PowerPanel - response = self.client.get(url) - self.assertEqual(response.status_code, 200) + # Disable inapplicable tests + test_bulk_edit_objects = None + + @classmethod + def setUpTestData(cls): + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(sites) + + rackgroups = ( + RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]), + RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]), + ) + RackGroup.objects.bulk_create(rackgroups) + + PowerPanel.objects.bulk_create(( + PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 1'), + PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 2'), + PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 3'), + )) + + cls.form_data = { + 'site': sites[1].pk, + 'rack_group': rackgroups[1].pk, + 'name': 'Power Panel X', + } + + cls.csv_data = ( + "site,rack_group_name,name", + "Site 1,Rack Group 1,Power Panel 4", + "Site 1,Rack Group 1,Power Panel 5", + "Site 1,Rack Group 1,Power Panel 6", + ) + + +class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = PowerFeed + + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') + + powerpanels = ( + PowerPanel(site=site, name='Power Panel 1'), + PowerPanel(site=site, name='Power Panel 2'), + ) + PowerPanel.objects.bulk_create(powerpanels) + + racks = ( + Rack(site=site, name='Rack 1'), + Rack(site=site, name='Rack 2'), + ) + Rack.objects.bulk_create(racks) + + PowerFeed.objects.bulk_create(( + PowerFeed(name='Power Feed 1', power_panel=powerpanels[0], rack=racks[0]), + PowerFeed(name='Power Feed 2', power_panel=powerpanels[0], rack=racks[0]), + PowerFeed(name='Power Feed 3', power_panel=powerpanels[0], rack=racks[0]), + )) + + cls.form_data = { + 'name': 'Power Feed X', + 'power_panel': powerpanels[1].pk, + 'rack': racks[1].pk, + 'status': PowerFeedStatusChoices.STATUS_PLANNED, + 'type': PowerFeedTypeChoices.TYPE_REDUNDANT, + 'supply': PowerFeedSupplyChoices.SUPPLY_DC, + 'phase': PowerFeedPhaseChoices.PHASE_3PHASE, + 'voltage': 100, + 'amperage': 100, + 'max_utilization': 50, + 'comments': 'New comments', + 'tags': 'Alpha,Bravo,Charlie', + + # Connection + 'cable': None, + 'connected_endpoint': None, + 'connection_status': None, + } + + cls.csv_data = ( + "site,panel_name,name,voltage,amperage,max_utilization", + "Site 1,Power Panel 1,Power Feed 4,120,20,80", + "Site 1,Power Panel 1,Power Feed 5,120,20,80", + "Site 1,Power Panel 1,Power Feed 6,120,20,80", + ) + + cls.bulk_edit_data = { + 'power_panel': powerpanels[1].pk, + 'rack': racks[1].pk, + 'status': PowerFeedStatusChoices.STATUS_PLANNED, + 'type': PowerFeedTypeChoices.TYPE_REDUNDANT, + 'supply': PowerFeedSupplyChoices.SUPPLY_DC, + 'phase': PowerFeedPhaseChoices.PHASE_3PHASE, + 'voltage': 100, + 'amperage': 100, + 'max_utilization': 50, + 'comments': 'New comments', + } diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index c3e852d1e..165ca9e02 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -14,301 +14,338 @@ app_name = 'dcim' urlpatterns = [ # Regions - path(r'regions/', views.RegionListView.as_view(), name='region_list'), - path(r'regions/add/', views.RegionCreateView.as_view(), name='region_add'), - path(r'regions/import/', views.RegionBulkImportView.as_view(), name='region_import'), - path(r'regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), - path(r'regions//edit/', views.RegionEditView.as_view(), name='region_edit'), - path(r'regions//changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), + path('regions/', views.RegionListView.as_view(), name='region_list'), + path('regions/add/', views.RegionCreateView.as_view(), name='region_add'), + path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'), + path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), + path('regions//edit/', views.RegionEditView.as_view(), name='region_edit'), + path('regions//changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), # Sites - path(r'sites/', views.SiteListView.as_view(), name='site_list'), - path(r'sites/add/', views.SiteCreateView.as_view(), name='site_add'), - path(r'sites/import/', views.SiteBulkImportView.as_view(), name='site_import'), - path(r'sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), - path(r'sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'), - path(r'sites//', views.SiteView.as_view(), name='site'), - path(r'sites//edit/', views.SiteEditView.as_view(), name='site_edit'), - path(r'sites//delete/', views.SiteDeleteView.as_view(), name='site_delete'), - path(r'sites//changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), - path(r'sites//images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), + path('sites/', views.SiteListView.as_view(), name='site_list'), + path('sites/add/', views.SiteCreateView.as_view(), name='site_add'), + path('sites/import/', views.SiteBulkImportView.as_view(), name='site_import'), + path('sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), + path('sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'), + path('sites//', views.SiteView.as_view(), name='site'), + path('sites//edit/', views.SiteEditView.as_view(), name='site_edit'), + path('sites//delete/', views.SiteDeleteView.as_view(), name='site_delete'), + path('sites//changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), + path('sites//images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), # Rack groups - path(r'rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'), - path(r'rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'), - path(r'rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), - path(r'rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), - path(r'rack-groups//edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'), - path(r'rack-groups//changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), + path('rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'), + path('rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'), + path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), + path('rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), + path('rack-groups//edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'), + path('rack-groups//changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), # Rack roles - path(r'rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'), - path(r'rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'), - path(r'rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), - path(r'rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), - path(r'rack-roles//edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'), - path(r'rack-roles//changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), + path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'), + path('rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'), + path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), + path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), + path('rack-roles//edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'), + path('rack-roles//changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), # Rack reservations - path(r'rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), - path(r'rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), - path(r'rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), - path(r'rack-reservations//edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), - path(r'rack-reservations//delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), - path(r'rack-reservations//changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), + path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), + path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), + path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), + path('rack-reservations//edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), + path('rack-reservations//delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), + path('rack-reservations//changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), # Racks - path(r'racks/', views.RackListView.as_view(), name='rack_list'), - path(r'rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'), - path(r'racks/add/', views.RackEditView.as_view(), name='rack_add'), - path(r'racks/import/', views.RackBulkImportView.as_view(), name='rack_import'), - path(r'racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), - path(r'racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'), - path(r'racks//', views.RackView.as_view(), name='rack'), - path(r'racks//edit/', views.RackEditView.as_view(), name='rack_edit'), - path(r'racks//delete/', views.RackDeleteView.as_view(), name='rack_delete'), - path(r'racks//changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), - path(r'racks//reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), - path(r'racks//images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), + path('racks/', views.RackListView.as_view(), name='rack_list'), + path('rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'), + path('racks/add/', views.RackCreateView.as_view(), name='rack_add'), + path('racks/import/', views.RackBulkImportView.as_view(), name='rack_import'), + path('racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), + path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'), + path('racks//', views.RackView.as_view(), name='rack'), + path('racks//edit/', views.RackEditView.as_view(), name='rack_edit'), + path('racks//delete/', views.RackDeleteView.as_view(), name='rack_delete'), + path('racks//changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), + path('racks//reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), + path('racks//images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), # Manufacturers - path(r'manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'), - path(r'manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), - path(r'manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), - path(r'manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), - path(r'manufacturers//edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), - path(r'manufacturers//changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), + path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'), + path('manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), + path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), + path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), + path('manufacturers//edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), + path('manufacturers//changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), # Device types - path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), - path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), - path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'), - path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), - path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), - path(r'device-types//', views.DeviceTypeView.as_view(), name='devicetype'), - path(r'device-types//edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), - path(r'device-types//delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), - path(r'device-types//changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), + path('device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), + path('device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), + path('device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'), + path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), + path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), + path('device-types//', views.DeviceTypeView.as_view(), name='devicetype'), + path('device-types//edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), + path('device-types//delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), + path('device-types//changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), # Console port templates - path(r'device-types//console-ports/add/', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), - path(r'device-types//console-ports/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'), - path(r'console-port-templates//edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'), + path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'), + path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'), + path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'), + path('console-port-templates//edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'), + path('console-port-templates//delete/', views.ConsolePortTemplateDeleteView.as_view(), name='consoleporttemplate_delete'), # Console server port templates - path(r'device-types//console-server-ports/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'), - path(r'device-types//console-server-ports/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'), - path(r'console-server-port-templates//edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'), + path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'), + path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'), + path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'), + path('console-server-port-templates//edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'), + path('console-server-port-templates//delete/', views.ConsoleServerPortTemplateDeleteView.as_view(), name='consoleserverporttemplate_delete'), # Power port templates - path(r'device-types//power-ports/add/', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'), - path(r'device-types//power-ports/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'), - path(r'power-port-templates//edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'), + path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'), + path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'), + path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'), + path('power-port-templates//edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'), + path('power-port-templates//delete/', views.PowerPortTemplateDeleteView.as_view(), name='powerporttemplate_delete'), # Power outlet templates - path(r'device-types//power-outlets/add/', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'), - path(r'device-types//power-outlets/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'), - path(r'power-outlet-templates//edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'), + path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'), + path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'), + path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'), + path('power-outlet-templates//edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'), + path('power-outlet-templates//delete/', views.PowerOutletTemplateDeleteView.as_view(), name='poweroutlettemplate_delete'), # Interface templates - path(r'device-types//interfaces/add/', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'), - path(r'device-types//interfaces/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'), - path(r'device-types//interfaces/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'), - path(r'interface-templates//edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'), + path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'), + path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'), + path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'), + path('interface-templates//edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'), + path('interface-templates//delete/', views.InterfaceTemplateDeleteView.as_view(), name='interfacetemplate_delete'), # Front port templates - path(r'device-types//front-ports/add/', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'), - path(r'device-types//front-ports/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'), - path(r'front-port-templates//edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'), + path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'), + path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'), + path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'), + path('front-port-templates//edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'), + path('front-port-templates//delete/', views.FrontPortTemplateDeleteView.as_view(), name='frontporttemplate_delete'), # Rear port templates - path(r'device-types//rear-ports/add/', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'), - path(r'device-types//rear-ports/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'), - path(r'rear-port-templates//edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'), + path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'), + path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'), + path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'), + path('rear-port-templates//edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'), + path('rear-port-templates//delete/', views.RearPortTemplateDeleteView.as_view(), name='rearporttemplate_delete'), # Device bay templates - path(r'device-types//device-bays/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'), - path(r'device-types//device-bays/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'), - path(r'device-bay-templates//edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'), + path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'), + # path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'), + path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'), + path('device-bay-templates//edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'), + path('device-bay-templates//delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'), # Device roles - path(r'device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'), - path(r'device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), - path(r'device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'), - path(r'device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), - path(r'device-roles//edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), - path(r'device-roles//changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), + path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'), + path('device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), + path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'), + path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), + path('device-roles//edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), + path('device-roles//changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), # Platforms - path(r'platforms/', views.PlatformListView.as_view(), name='platform_list'), - path(r'platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'), - path(r'platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'), - path(r'platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), - path(r'platforms//edit/', views.PlatformEditView.as_view(), name='platform_edit'), - path(r'platforms//changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), + path('platforms/', views.PlatformListView.as_view(), name='platform_list'), + path('platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'), + path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'), + path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), + path('platforms//edit/', views.PlatformEditView.as_view(), name='platform_edit'), + path('platforms//changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), # Devices - path(r'devices/', views.DeviceListView.as_view(), name='device_list'), - path(r'devices/add/', views.DeviceCreateView.as_view(), name='device_add'), - path(r'devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'), - path(r'devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), - path(r'devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), - path(r'devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), - path(r'devices//', views.DeviceView.as_view(), name='device'), - path(r'devices//edit/', views.DeviceEditView.as_view(), name='device_edit'), - path(r'devices//delete/', views.DeviceDeleteView.as_view(), name='device_delete'), - path(r'devices//config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'), - path(r'devices//changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), - path(r'devices//inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'), - path(r'devices//status/', views.DeviceStatusView.as_view(), name='device_status'), - path(r'devices//lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), - path(r'devices//config/', views.DeviceConfigView.as_view(), name='device_config'), - path(r'devices//add-secret/', secret_add, name='device_addsecret'), - path(r'devices//services/assign/', ServiceCreateView.as_view(), name='device_service_assign'), - path(r'devices//images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), + path('devices/', views.DeviceListView.as_view(), name='device_list'), + path('devices/add/', views.DeviceCreateView.as_view(), name='device_add'), + path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'), + path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), + path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), + path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), + path('devices//', views.DeviceView.as_view(), name='device'), + path('devices//edit/', views.DeviceEditView.as_view(), name='device_edit'), + path('devices//delete/', views.DeviceDeleteView.as_view(), name='device_delete'), + path('devices//config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'), + path('devices//changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), + path('devices//inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'), + path('devices//status/', views.DeviceStatusView.as_view(), name='device_status'), + path('devices//lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), + path('devices//config/', views.DeviceConfigView.as_view(), name='device_config'), + path('devices//add-secret/', secret_add, name='device_addsecret'), + path('devices//services/assign/', ServiceCreateView.as_view(), name='device_service_assign'), + path('devices//images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), # Console ports - path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), - path(r'devices//console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), - path(r'devices//console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), - path(r'console-ports//connect//', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), - path(r'console-ports//edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), - path(r'console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), - path(r'console-ports//trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), + path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'), + path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), + path('console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'), + path('console-ports/edit/', views.ConsolePortBulkEditView.as_view(), name='consoleport_bulk_edit'), + # TODO: Bulk rename, disconnect views for ConsolePorts + path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), + path('console-ports//connect//', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), + path('console-ports//edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), + path('console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), + path('console-ports//trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), + path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), # Console server ports - path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), - path(r'devices//console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), - path(r'devices//console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'), - path(r'devices//console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), - path(r'console-server-ports//connect//', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), - path(r'console-server-ports//edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), - path(r'console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), - path(r'console-server-ports//trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), - path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), - path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), + path('console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'), + path('console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), + path('console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'), + path('console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'), + path('console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), + path('console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), + path('console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), + path('console-server-ports//connect//', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), + path('console-server-ports//edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), + path('console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), + path('console-server-ports//trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), + path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), # Power ports - path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), - path(r'devices//power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), - path(r'devices//power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), - path(r'power-ports//connect//', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), - path(r'power-ports//edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), - path(r'power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), - path(r'power-ports//trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), + path('power-ports/', views.PowerPortListView.as_view(), name='powerport_list'), + path('power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), + path('power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'), + path('power-ports/edit/', views.PowerPortBulkEditView.as_view(), name='powerport_bulk_edit'), + # TODO: Bulk rename, disconnect views for PowerPorts + path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), + path('power-ports//connect//', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), + path('power-ports//edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), + path('power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), + path('power-ports//trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), + path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), # Power outlets - path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), - path(r'devices//power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), - path(r'devices//power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'), - path(r'devices//power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), - path(r'power-outlets//connect//', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), - path(r'power-outlets//edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), - path(r'power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), - path(r'power-outlets//trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), - path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), - path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), + path('power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'), + path('power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), + path('power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'), + path('power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'), + path('power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), + path('power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), + path('power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), + path('power-outlets//connect//', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), + path('power-outlets//edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), + path('power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), + path('power-outlets//trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), + path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), # Interfaces - path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), - path(r'devices//interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), - path(r'devices//interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), - path(r'devices//interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), - path(r'interfaces//connect//', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), - path(r'interfaces//', views.InterfaceView.as_view(), name='interface'), - path(r'interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), - path(r'interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), - path(r'interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), - path(r'interfaces//trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), - path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), - path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), + path('interfaces/', views.InterfaceListView.as_view(), name='interface_list'), + path('interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), + path('interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'), + path('interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), + path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), + path('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), + path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), + path('interfaces//connect//', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), + path('interfaces//', views.InterfaceView.as_view(), name='interface'), + path('interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), + path('interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), + path('interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), + path('interfaces//trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), + path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), # Front ports - # path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), - path(r'devices//front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'), - path(r'devices//front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), - path(r'devices//front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), - path(r'front-ports//connect//', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), - path(r'front-ports//edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), - path(r'front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), - path(r'front-ports//trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), - path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), - path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), + path('front-ports/', views.FrontPortListView.as_view(), name='frontport_list'), + path('front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'), + path('front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'), + path('front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), + path('front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), + path('front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), + path('front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), + path('front-ports//connect//', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), + path('front-ports//edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), + path('front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), + path('front-ports//trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), + # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # Rear ports - # path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), - path(r'devices//rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'), - path(r'devices//rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), - path(r'devices//rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), - path(r'rear-ports//connect//', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), - path(r'rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'), - path(r'rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), - path(r'rear-ports//trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), - path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), - path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), + path('rear-ports/', views.RearPortListView.as_view(), name='rearport_list'), + path('rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'), + path('rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'), + path('rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), + path('rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), + path('rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), + path('rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), + path('rear-ports//connect//', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), + path('rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'), + path('rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), + path('rear-ports//trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), + # path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), # Device bays - path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), - path(r'devices//bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), - path(r'devices//bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), - path(r'device-bays//edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), - path(r'device-bays//delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), - path(r'device-bays//populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), - path(r'device-bays//depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), - path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), + path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'), + path('device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), + path('device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'), + # TODO: Bulk edit view for DeviceBays + path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), + path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), + path('device-bays//edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), + path('device-bays//delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), + path('device-bays//populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), + path('device-bays//depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), + path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), # Inventory items - path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), - path(r'inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'), - path(r'inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'), - path(r'inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'), - path(r'inventory-items//edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), - path(r'inventory-items//delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), - path(r'devices//inventory-items/add/', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), + path('inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), + path('inventory-items/add/', views.InventoryItemCreateView.as_view(), name='inventoryitem_add'), + path('inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'), + path('inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'), + # TODO: Bulk rename view for InventoryItems + path('inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'), + path('inventory-items//edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), + path('inventory-items//delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), # Cables - path(r'cables/', views.CableListView.as_view(), name='cable_list'), - path(r'cables/import/', views.CableBulkImportView.as_view(), name='cable_import'), - path(r'cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'), - path(r'cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'), - path(r'cables//', views.CableView.as_view(), name='cable'), - path(r'cables//edit/', views.CableEditView.as_view(), name='cable_edit'), - path(r'cables//delete/', views.CableDeleteView.as_view(), name='cable_delete'), - path(r'cables//changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}), + path('cables/', views.CableListView.as_view(), name='cable_list'), + path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'), + path('cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'), + path('cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'), + path('cables//', views.CableView.as_view(), name='cable'), + path('cables//edit/', views.CableEditView.as_view(), name='cable_edit'), + path('cables//delete/', views.CableDeleteView.as_view(), name='cable_delete'), + path('cables//changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}), # Console/power/interface connections (read-only) - path(r'console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), - path(r'power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'), - path(r'interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'), + path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), + path('power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'), + path('interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'), # Virtual chassis - path(r'virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), - path(r'virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), - path(r'virtual-chassis//edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), - path(r'virtual-chassis//delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), - path(r'virtual-chassis//changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), - path(r'virtual-chassis//add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), - path(r'virtual-chassis-members//delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), + path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), + path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), + path('virtual-chassis//edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), + path('virtual-chassis//delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), + path('virtual-chassis//changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), + path('virtual-chassis//add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), + path('virtual-chassis-members//delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), # Power panels - path(r'power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'), - path(r'power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'), - path(r'power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'), - path(r'power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'), - path(r'power-panels//', views.PowerPanelView.as_view(), name='powerpanel'), - path(r'power-panels//edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), - path(r'power-panels//delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), - path(r'power-panels//changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), + path('power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'), + path('power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'), + path('power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'), + path('power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'), + path('power-panels//', views.PowerPanelView.as_view(), name='powerpanel'), + path('power-panels//edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), + path('power-panels//delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), + path('power-panels//changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), # Power feeds - path(r'power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'), - path(r'power-feeds/add/', views.PowerFeedEditView.as_view(), name='powerfeed_add'), - path(r'power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'), - path(r'power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'), - path(r'power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'), - path(r'power-feeds//', views.PowerFeedView.as_view(), name='powerfeed'), - path(r'power-feeds//edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), - path(r'power-feeds//delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), - path(r'power-feeds//changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), + path('power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'), + path('power-feeds/add/', views.PowerFeedCreateView.as_view(), name='powerfeed_add'), + path('power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'), + path('power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'), + path('power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'), + path('power-feeds//', views.PowerFeedView.as_view(), name='powerfeed'), + path('power-feeds//edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), + path('power-feeds//delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), + path('power-feeds//changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 959e1043e..8f9da2d68 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,3 +1,4 @@ +from collections import OrderedDict import re from django.conf import settings @@ -16,8 +17,7 @@ from django.utils.safestring import mark_safe from django.views.generic import View from circuits.models import Circuit -from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE -from extras.models import Graph, TopologyMap +from extras.models import Graph from extras.views import ObjectConfigContextView from ipam.models import Prefix, VLAN from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable @@ -26,10 +26,12 @@ from utilities.paginator import EnhancedPaginator from utilities.utils import csv_format from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin, - ObjectDeleteView, ObjectEditView, ObjectListView, + ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from virtualization.models import VirtualMachine from . import filters, forms, tables +from .choices import DeviceFaceChoices +from .constants import NONCONNECTABLE_IFACE_TYPES from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, @@ -148,10 +150,9 @@ class RegionListView(PermissionRequiredMixin, ObjectListView): 'site_count', cumulative=True ) - filter = filters.RegionFilter - filter_form = forms.RegionFilterForm + filterset = filters.RegionFilterSet + filterset_form = forms.RegionFilterForm table = tables.RegionTable - template_name = 'dcim/region_list.html' class RegionCreateView(PermissionRequiredMixin, ObjectEditView): @@ -175,7 +176,7 @@ class RegionBulkImportView(PermissionRequiredMixin, BulkImportView): class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_region' queryset = Region.objects.all() - filter = filters.RegionFilter + filterset = filters.RegionFilterSet table = tables.RegionTable default_return_url = 'dcim:region_list' @@ -187,10 +188,9 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class SiteListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_site' queryset = Site.objects.prefetch_related('region', 'tenant') - filter = filters.SiteFilter - filter_form = forms.SiteFilterForm + filterset = filters.SiteFilterSet + filterset_form = forms.SiteFilterForm table = tables.SiteTable - template_name = 'dcim/site_list.html' class SiteView(PermissionRequiredMixin, View): @@ -208,14 +208,12 @@ class SiteView(PermissionRequiredMixin, View): 'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(), } rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) - topology_maps = TopologyMap.objects.filter(site=site) - show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists() + show_graphs = Graph.objects.filter(type__model='site').exists() return render(request, 'dcim/site.html', { 'site': site, 'stats': stats, 'rack_groups': rack_groups, - 'topology_maps': topology_maps, 'show_graphs': show_graphs, }) @@ -248,7 +246,7 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView): class SiteBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_site' queryset = Site.objects.prefetch_related('region', 'tenant') - filter = filters.SiteFilter + filterset = filters.SiteFilterSet table = tables.SiteTable form = forms.SiteBulkEditForm default_return_url = 'dcim:site_list' @@ -257,7 +255,7 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView): class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_site' queryset = Site.objects.prefetch_related('region', 'tenant') - filter = filters.SiteFilter + filterset = filters.SiteFilterSet table = tables.SiteTable default_return_url = 'dcim:site_list' @@ -269,10 +267,9 @@ class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackGroupListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_rackgroup' queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks')) - filter = filters.RackGroupFilter - filter_form = forms.RackGroupFilterForm + filterset = filters.RackGroupFilterSet + filterset_form = forms.RackGroupFilterForm table = tables.RackGroupTable - template_name = 'dcim/rackgroup_list.html' class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView): @@ -296,7 +293,7 @@ class RackGroupBulkImportView(PermissionRequiredMixin, BulkImportView): class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackgroup' queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks')) - filter = filters.RackGroupFilter + filterset = filters.RackGroupFilterSet table = tables.RackGroupTable default_return_url = 'dcim:rackgroup_list' @@ -309,7 +306,6 @@ class RackRoleListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_rackrole' queryset = RackRole.objects.annotate(rack_count=Count('racks')) table = tables.RackRoleTable - template_name = 'dcim/rackrole_list.html' class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView): @@ -348,10 +344,9 @@ class RackListView(PermissionRequiredMixin, ObjectListView): ).annotate( device_count=Count('devices') ) - filter = filters.RackFilter - filter_form = forms.RackFilterForm + filterset = filters.RackFilterSet + filterset_form = forms.RackFilterForm table = tables.RackDetailTable - template_name = 'dcim/rack_list.html' class RackElevationListView(PermissionRequiredMixin, View): @@ -362,8 +357,8 @@ class RackElevationListView(PermissionRequiredMixin, View): def get(self, request): - racks = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role', 'devices__device_type') - racks = filters.RackFilter(request.GET, racks).qs + racks = Rack.objects.prefetch_related('role') + racks = filters.RackFilterSet(request.GET, racks).qs total_count = racks.count() # Pagination @@ -378,17 +373,16 @@ class RackElevationListView(PermissionRequiredMixin, View): page = paginator.page(paginator.num_pages) # Determine rack face - if request.GET.get('face') == '1': - face_id = 1 - else: - face_id = 0 + rack_face = request.GET.get('face', DeviceFaceChoices.FACE_FRONT) + if rack_face not in DeviceFaceChoices.values(): + rack_face = DeviceFaceChoices.FACE_FRONT return render(request, 'dcim/rack_elevation_list.html', { 'paginator': paginator, 'page': page, 'total_count': total_count, - 'face_id': face_id, - 'filter_form': forms.RackFilterForm(request.GET), + 'rack_face': rack_face, + 'filter_form': forms.RackElevationFilterForm(request.GET), }) @@ -421,8 +415,6 @@ class RackView(PermissionRequiredMixin, View): 'nonracked_devices': nonracked_devices, 'next_rack': next_rack, 'prev_rack': prev_rack, - 'front_elevation': rack.get_front_elevation(), - 'rear_elevation': rack.get_rear_elevation(), }) @@ -454,7 +446,7 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView): class RackBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rack' queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role') - filter = filters.RackFilter + filterset = filters.RackFilterSet table = tables.RackTable form = forms.RackBulkEditForm default_return_url = 'dcim:rack_list' @@ -463,7 +455,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView): class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rack' queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role') - filter = filters.RackFilter + filterset = filters.RackFilterSet table = tables.RackTable default_return_url = 'dcim:rack_list' @@ -475,10 +467,10 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackReservationListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_rackreservation' queryset = RackReservation.objects.prefetch_related('rack__site') - filter = filters.RackReservationFilter - filter_form = forms.RackReservationFilterForm + filterset = filters.RackReservationFilterSet + filterset_form = forms.RackReservationFilterForm table = tables.RackReservationTable - template_name = 'dcim/rackreservation_list.html' + action_buttons = () class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView): @@ -511,7 +503,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rackreservation' queryset = RackReservation.objects.prefetch_related('rack', 'user') - filter = filters.RackReservationFilter + filterset = filters.RackReservationFilterSet table = tables.RackReservationTable form = forms.RackReservationBulkEditForm default_return_url = 'dcim:rackreservation_list' @@ -520,7 +512,7 @@ class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView): class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackreservation' queryset = RackReservation.objects.prefetch_related('rack', 'user') - filter = filters.RackReservationFilter + filterset = filters.RackReservationFilterSet table = tables.RackReservationTable default_return_url = 'dcim:rackreservation_list' @@ -537,7 +529,6 @@ class ManufacturerListView(PermissionRequiredMixin, ObjectListView): platform_count=Count('platforms', distinct=True), ) table = tables.ManufacturerTable - template_name = 'dcim/manufacturer_list.html' class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView): @@ -572,10 +563,9 @@ class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceTypeListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_devicetype' queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')) - filter = filters.DeviceTypeFilter - filter_form = forms.DeviceTypeFilterForm + filterset = filters.DeviceTypeFilterSet + filterset_form = forms.DeviceTypeFilterForm table = tables.DeviceTypeTable - template_name = 'dcim/devicetype_list.html' class DeviceTypeView(PermissionRequiredMixin, View): @@ -659,17 +649,37 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): default_return_url = 'dcim:devicetype_list' -class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView): - permission_required = 'dcim.add_devicetype' - model_form = forms.DeviceTypeCSVForm - table = tables.DeviceTypeTable - default_return_url = 'dcim:devicetype_list' +class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView): + permission_required = [ + 'dcim.add_devicetype', + 'dcim.add_consoleporttemplate', + 'dcim.add_consoleserverporttemplate', + 'dcim.add_powerporttemplate', + 'dcim.add_poweroutlettemplate', + 'dcim.add_interfacetemplate', + 'dcim.add_frontporttemplate', + 'dcim.add_rearporttemplate', + 'dcim.add_devicebaytemplate', + ] + model = DeviceType + model_form = forms.DeviceTypeImportForm + related_object_forms = OrderedDict(( + ('console-ports', forms.ConsolePortTemplateImportForm), + ('console-server-ports', forms.ConsoleServerPortTemplateImportForm), + ('power-ports', forms.PowerPortTemplateImportForm), + ('power-outlets', forms.PowerOutletTemplateImportForm), + ('interfaces', forms.InterfaceTemplateImportForm), + ('rear-ports', forms.RearPortTemplateImportForm), + ('front-ports', forms.FrontPortTemplateImportForm), + ('device-bays', forms.DeviceBayTemplateImportForm), + )) + default_return_url = 'dcim:devicetype_import' class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_devicetype' queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')) - filter = filters.DeviceTypeFilter + filterset = filters.DeviceTypeFilterSet table = tables.DeviceTypeTable form = forms.DeviceTypeBulkEditForm default_return_url = 'dcim:devicetype_list' @@ -678,19 +688,17 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView): class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicetype' queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')) - filter = filters.DeviceTypeFilter + filterset = filters.DeviceTypeFilterSet table = tables.DeviceTypeTable default_return_url = 'dcim:devicetype_list' # -# Device type components +# Console port templates # class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = ConsolePortTemplate form = forms.ConsolePortTemplateCreateForm model_form = forms.ConsolePortTemplateForm @@ -703,17 +711,30 @@ class ConsolePortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.ConsolePortTemplateForm +class ConsolePortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_consoleporttemplate' + model = ConsolePortTemplate + + +class ConsolePortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_consoleporttemplate' + queryset = ConsolePortTemplate.objects.all() + table = tables.ConsolePortTemplateTable + form = forms.ConsolePortTemplateBulkEditForm + + class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleporttemplate' queryset = ConsolePortTemplate.objects.all() - parent_model = DeviceType table = tables.ConsolePortTemplateTable +# +# Console server port templates +# + class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleserverporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = ConsoleServerPortTemplate form = forms.ConsoleServerPortTemplateCreateForm model_form = forms.ConsoleServerPortTemplateForm @@ -726,17 +747,30 @@ class ConsoleServerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView) model_form = forms.ConsoleServerPortTemplateForm +class ConsoleServerPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_consoleserverporttemplate' + model = ConsoleServerPortTemplate + + +class ConsoleServerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_consoleserverporttemplate' + queryset = ConsoleServerPortTemplate.objects.all() + table = tables.ConsoleServerPortTemplateTable + form = forms.ConsoleServerPortTemplateBulkEditForm + + class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleserverporttemplate' queryset = ConsoleServerPortTemplate.objects.all() - parent_model = DeviceType table = tables.ConsoleServerPortTemplateTable +# +# Power port templates +# + class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_powerporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = PowerPortTemplate form = forms.PowerPortTemplateCreateForm model_form = forms.PowerPortTemplateForm @@ -749,17 +783,30 @@ class PowerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.PowerPortTemplateForm +class PowerPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_powerporttemplate' + model = PowerPortTemplate + + +class PowerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_powerporttemplate' + queryset = PowerPortTemplate.objects.all() + table = tables.PowerPortTemplateTable + form = forms.PowerPortTemplateBulkEditForm + + class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerporttemplate' queryset = PowerPortTemplate.objects.all() - parent_model = DeviceType table = tables.PowerPortTemplateTable +# +# Power outlet templates +# + class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_poweroutlettemplate' - parent_model = DeviceType - parent_field = 'device_type' model = PowerOutletTemplate form = forms.PowerOutletTemplateCreateForm model_form = forms.PowerOutletTemplateForm @@ -772,17 +819,30 @@ class PowerOutletTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.PowerOutletTemplateForm +class PowerOutletTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_poweroutlettemplate' + model = PowerOutletTemplate + + +class PowerOutletTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_poweroutlettemplate' + queryset = PowerOutletTemplate.objects.all() + table = tables.PowerOutletTemplateTable + form = forms.PowerOutletTemplateBulkEditForm + + class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_poweroutlettemplate' queryset = PowerOutletTemplate.objects.all() - parent_model = DeviceType table = tables.PowerOutletTemplateTable +# +# Interface templates +# + class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_interfacetemplate' - parent_model = DeviceType - parent_field = 'device_type' model = InterfaceTemplate form = forms.InterfaceTemplateCreateForm model_form = forms.InterfaceTemplateForm @@ -795,10 +855,14 @@ class InterfaceTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.InterfaceTemplateForm +class InterfaceTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_interfacetemplate' + model = InterfaceTemplate + + class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_interfacetemplate' queryset = InterfaceTemplate.objects.all() - parent_model = DeviceType table = tables.InterfaceTemplateTable form = forms.InterfaceTemplateBulkEditForm @@ -806,14 +870,15 @@ class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_interfacetemplate' queryset = InterfaceTemplate.objects.all() - parent_model = DeviceType table = tables.InterfaceTemplateTable +# +# Front port templates +# + class FrontPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_frontporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = FrontPortTemplate form = forms.FrontPortTemplateCreateForm model_form = forms.FrontPortTemplateForm @@ -826,17 +891,30 @@ class FrontPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.FrontPortTemplateForm +class FrontPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_frontporttemplate' + model = FrontPortTemplate + + +class FrontPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_frontporttemplate' + queryset = FrontPortTemplate.objects.all() + table = tables.FrontPortTemplateTable + form = forms.FrontPortTemplateBulkEditForm + + class FrontPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_frontporttemplate' queryset = FrontPortTemplate.objects.all() - parent_model = DeviceType table = tables.FrontPortTemplateTable +# +# Rear port templates +# + class RearPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_rearporttemplate' - parent_model = DeviceType - parent_field = 'device_type' model = RearPortTemplate form = forms.RearPortTemplateCreateForm model_form = forms.RearPortTemplateForm @@ -849,17 +927,30 @@ class RearPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.RearPortTemplateForm +class RearPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_rearporttemplate' + model = RearPortTemplate + + +class RearPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_rearporttemplate' + queryset = RearPortTemplate.objects.all() + table = tables.RearPortTemplateTable + form = forms.RearPortTemplateBulkEditForm + + class RearPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rearporttemplate' queryset = RearPortTemplate.objects.all() - parent_model = DeviceType table = tables.RearPortTemplateTable +# +# Device bay templates +# + class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_devicebaytemplate' - parent_model = DeviceType - parent_field = 'device_type' model = DeviceBayTemplate form = forms.DeviceBayTemplateCreateForm model_form = forms.DeviceBayTemplateForm @@ -872,10 +963,21 @@ class DeviceBayTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.DeviceBayTemplateForm +class DeviceBayTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_devicebaytemplate' + model = DeviceBayTemplate + + +# class DeviceBayTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): +# permission_required = 'dcim.change_devicebaytemplate' +# queryset = DeviceBayTemplate.objects.all() +# table = tables.DeviceBayTemplateTable +# form = forms.DeviceBayTemplateBulkEditForm + + class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicebaytemplate' queryset = DeviceBayTemplate.objects.all() - parent_model = DeviceType table = tables.DeviceBayTemplateTable @@ -887,7 +989,6 @@ class DeviceRoleListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_devicerole' queryset = DeviceRole.objects.all() table = tables.DeviceRoleTable - template_name = 'dcim/devicerole_list.html' class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView): @@ -923,7 +1024,6 @@ class PlatformListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_platform' queryset = Platform.objects.all() table = tables.PlatformTable - template_name = 'dcim/platform_list.html' class PlatformCreateView(PermissionRequiredMixin, ObjectEditView): @@ -960,8 +1060,8 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView): queryset = Device.objects.prefetch_related( 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6' ) - filter = filters.DeviceFilter - filter_form = forms.DeviceFilterForm + filterset = filters.DeviceFilterSet + filterset_form = forms.DeviceFilterForm table = tables.DeviceDetailTable template_name = 'dcim/device_list.html' @@ -1039,8 +1139,8 @@ class DeviceView(PermissionRequiredMixin, View): 'secrets': secrets, 'vc_members': vc_members, 'related_devices': related_devices, - 'show_graphs': Graph.objects.filter(type=GRAPH_TYPE_DEVICE).exists(), - 'show_interface_graphs': Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists(), + 'show_graphs': Graph.objects.filter(type__model='device').exists(), + 'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(), }) @@ -1082,7 +1182,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View): def get(self, request, pk): device = get_object_or_404(Device, pk=pk) - interfaces = device.vc_interfaces.connectable().prefetch_related( + interfaces = device.vc_interfaces.exclude(type__in=NONCONNECTABLE_IFACE_TYPES).prefetch_related( '_connected_interface__device' ) @@ -1160,7 +1260,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView): class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_device' queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable form = forms.DeviceBulkEditForm default_return_url = 'dcim:device_list' @@ -1169,7 +1269,7 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_device' queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1178,10 +1278,17 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Console ports # +class ConsolePortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_consoleport' + queryset = ConsolePort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.ConsolePortFilterSet + filterset_form = forms.ConsolePortFilterForm + table = tables.ConsolePortDetailTable + action_buttons = ('import', 'export') + + class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleport' - parent_model = Device - parent_field = 'device' model = ConsolePort form = forms.ConsolePortCreateForm model_form = forms.ConsolePortForm @@ -1199,21 +1306,42 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView): model = ConsolePort +class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_consoleport' + model_form = forms.ConsolePortCSVForm + table = tables.ConsolePortImportTable + default_return_url = 'dcim:consoleport_list' + + +class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_consoleport' + queryset = ConsolePort.objects.all() + table = tables.ConsolePortTable + form = forms.ConsolePortBulkEditForm + + class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleport' queryset = ConsolePort.objects.all() - parent_model = Device table = tables.ConsolePortTable + default_return_url = 'dcim:consoleport_list' # # Console server ports # +class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_consoleserverport' + queryset = ConsoleServerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.ConsoleServerPortFilterSet + filterset_form = forms.ConsoleServerPortFilterForm + table = tables.ConsoleServerPortDetailTable + action_buttons = ('import', 'export') + + class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleserverport' - parent_model = Device - parent_field = 'device' model = ConsoleServerPort form = forms.ConsoleServerPortCreateForm model_form = forms.ConsoleServerPortForm @@ -1231,10 +1359,16 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): model = ConsoleServerPort +class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_consoleserverport' + model_form = forms.ConsoleServerPortCSVForm + table = tables.ConsoleServerPortImportTable + default_return_url = 'dcim:consoleserverport_list' + + class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_consoleserverport' queryset = ConsoleServerPort.objects.all() - parent_model = Device table = tables.ConsoleServerPortTable form = forms.ConsoleServerPortBulkEditForm @@ -1254,18 +1388,25 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleserverport' queryset = ConsoleServerPort.objects.all() - parent_model = Device table = tables.ConsoleServerPortTable + default_return_url = 'dcim:consoleserverport_list' # # Power ports # +class PowerPortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_powerport' + queryset = PowerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.PowerPortFilterSet + filterset_form = forms.PowerPortFilterForm + table = tables.PowerPortDetailTable + action_buttons = ('import', 'export') + + class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_powerport' - parent_model = Device - parent_field = 'device' model = PowerPort form = forms.PowerPortCreateForm model_form = forms.PowerPortForm @@ -1283,21 +1424,42 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): model = PowerPort +class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_powerport' + model_form = forms.PowerPortCSVForm + table = tables.PowerPortImportTable + default_return_url = 'dcim:powerport_list' + + +class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_powerport' + queryset = PowerPort.objects.all() + table = tables.PowerPortTable + form = forms.PowerPortBulkEditForm + + class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerport' queryset = PowerPort.objects.all() - parent_model = Device table = tables.PowerPortTable + default_return_url = 'dcim:powerport_list' # # Power outlets # +class PowerOutletListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_poweroutlet' + queryset = PowerOutlet.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.PowerOutletFilterSet + filterset_form = forms.PowerOutletFilterForm + table = tables.PowerOutletDetailTable + action_buttons = ('import', 'export') + + class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_poweroutlet' - parent_model = Device - parent_field = 'device' model = PowerOutlet form = forms.PowerOutletCreateForm model_form = forms.PowerOutletForm @@ -1315,10 +1477,16 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView): model = PowerOutlet +class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_poweroutlet' + model_form = forms.PowerOutletCSVForm + table = tables.PowerOutletImportTable + default_return_url = 'dcim:poweroutlet_list' + + class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_poweroutlet' queryset = PowerOutlet.objects.all() - parent_model = Device table = tables.PowerOutletTable form = forms.PowerOutletBulkEditForm @@ -1338,14 +1506,23 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView) class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_poweroutlet' queryset = PowerOutlet.objects.all() - parent_model = Device table = tables.PowerOutletTable + default_return_url = 'dcim:poweroutlet_list' # # Interfaces # +class InterfaceListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_interface' + queryset = Interface.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.InterfaceFilterSet + filterset_form = forms.InterfaceFilterForm + table = tables.InterfaceDetailTable + action_buttons = ('import', 'export') + + class InterfaceView(PermissionRequiredMixin, View): permission_required = 'dcim.view_interface' @@ -1384,8 +1561,6 @@ class InterfaceView(PermissionRequiredMixin, View): class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_interface' - parent_model = Device - parent_field = 'device' model = Interface form = forms.InterfaceCreateForm model_form = forms.InterfaceForm @@ -1404,10 +1579,16 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView): model = Interface +class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_interface' + model_form = forms.InterfaceCSVForm + table = tables.InterfaceImportTable + default_return_url = 'dcim:interface_list' + + class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_interface' queryset = Interface.objects.all() - parent_model = Device table = tables.InterfaceTable form = forms.InterfaceBulkEditForm @@ -1427,18 +1608,25 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_interface' queryset = Interface.objects.all() - parent_model = Device table = tables.InterfaceTable + default_return_url = 'dcim:interface_list' # # Front ports # +class FrontPortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_frontport' + queryset = FrontPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.FrontPortFilterSet + filterset_form = forms.FrontPortFilterForm + table = tables.FrontPortDetailTable + action_buttons = ('import', 'export') + + class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_frontport' - parent_model = Device - parent_field = 'device' model = FrontPort form = forms.FrontPortCreateForm model_form = forms.FrontPortForm @@ -1456,10 +1644,16 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): model = FrontPort +class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_frontport' + model_form = forms.FrontPortCSVForm + table = tables.FrontPortImportTable + default_return_url = 'dcim:frontport_list' + + class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_frontport' queryset = FrontPort.objects.all() - parent_model = Device table = tables.FrontPortTable form = forms.FrontPortBulkEditForm @@ -1479,18 +1673,25 @@ class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_frontport' queryset = FrontPort.objects.all() - parent_model = Device table = tables.FrontPortTable + default_return_url = 'dcim:frontport_list' # # Rear ports # +class RearPortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_rearport' + queryset = RearPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.RearPortFilterSet + filterset_form = forms.RearPortFilterForm + table = tables.RearPortDetailTable + action_buttons = ('import', 'export') + + class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_rearport' - parent_model = Device - parent_field = 'device' model = RearPort form = forms.RearPortCreateForm model_form = forms.RearPortForm @@ -1508,10 +1709,16 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): model = RearPort +class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_rearport' + model_form = forms.RearPortCSVForm + table = tables.RearPortImportTable + default_return_url = 'dcim:rearport_list' + + class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rearport' queryset = RearPort.objects.all() - parent_model = Device table = tables.RearPortTable form = forms.RearPortBulkEditForm @@ -1531,18 +1738,27 @@ class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rearport' queryset = RearPort.objects.all() - parent_model = Device table = tables.RearPortTable + default_return_url = 'dcim:rearport_list' # # Device bays # +class DeviceBayListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_devicebay' + queryset = DeviceBay.objects.prefetch_related( + 'device', 'device__site', 'installed_device', 'installed_device__site' + ) + filterset = filters.DeviceBayFilterSet + filterset_form = forms.DeviceBayFilterForm + table = tables.DeviceBayDetailTable + action_buttons = ('import', 'export') + + class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_devicebay' - parent_model = Device - parent_field = 'device' model = DeviceBay form = forms.DeviceBayCreateForm model_form = forms.DeviceBayForm @@ -1629,6 +1845,13 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View): }) +class DeviceBayBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_devicebay' + model_form = forms.DeviceBayCSVForm + table = tables.DeviceBayImportTable + default_return_url = 'dcim:devicebay_list' + + class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_devicebay' queryset = DeviceBay.objects.all() @@ -1638,8 +1861,8 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicebay' queryset = DeviceBay.objects.all() - parent_model = Device table = tables.DeviceBayTable + default_return_url = 'dcim:devicebay_list' # @@ -1653,7 +1876,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV form = forms.DeviceBulkAddComponentForm model = ConsolePort model_form = forms.ConsolePortForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1665,7 +1888,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC form = forms.DeviceBulkAddComponentForm model = ConsoleServerPort model_form = forms.ConsoleServerPortForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1677,7 +1900,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie form = forms.DeviceBulkAddComponentForm model = PowerPort model_form = forms.PowerPortForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1689,7 +1912,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV form = forms.DeviceBulkAddComponentForm model = PowerOutlet model_form = forms.PowerOutletForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1701,7 +1924,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie form = forms.DeviceBulkAddInterfaceForm model = Interface model_form = forms.InterfaceForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1713,7 +1936,7 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie form = forms.DeviceBulkAddComponentForm model = DeviceBay model_form = forms.DeviceBayForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1727,10 +1950,10 @@ class CableListView(PermissionRequiredMixin, ObjectListView): queryset = Cable.objects.prefetch_related( 'termination_a', 'termination_b' ) - filter = filters.CableFilter - filter_form = forms.CableFilterForm + filterset = filters.CableFilterSet + filterset_form = forms.CableFilterForm table = tables.CableTable - template_name = 'dcim/cable_list.html' + action_buttons = ('import', 'export') class CableView(PermissionRequiredMixin, View): @@ -1754,10 +1977,13 @@ class CableTraceView(PermissionRequiredMixin, View): def get(self, request, model, pk): obj = get_object_or_404(model, pk=pk) + trace = obj.trace(follow_circuits=True) + total_length = sum([entry[1]._abs_length for entry in trace if entry[1] and entry[1]._abs_length]) return render(request, 'dcim/cable_trace.html', { 'obj': obj, - 'trace': obj.trace(follow_circuits=True), + 'trace': trace, + 'total_length': total_length, }) @@ -1796,6 +2022,12 @@ class CableCreateView(PermissionRequiredMixin, GetReturnURLMixin, View): # Parse initial data manually to avoid setting field values as lists initial_data = {k: request.GET[k] for k in request.GET} + # Set initial site and rack based on side A termination (if not already set) + if 'termination_b_site' not in initial_data: + initial_data['termination_b_site'] = getattr(self.obj.termination_a.parent, 'site', None) + if 'termination_b_rack' not in initial_data: + initial_data['termination_b_rack'] = getattr(self.obj.termination_a.parent, 'rack', None) + form = self.form_class(instance=self.obj, initial=initial_data) return render(request, self.template_name, { @@ -1861,7 +2093,7 @@ class CableBulkImportView(PermissionRequiredMixin, BulkImportView): class CableBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_cable' queryset = Cable.objects.prefetch_related('termination_a', 'termination_b') - filter = filters.CableFilter + filterset = filters.CableFilterSet table = tables.CableTable form = forms.CableBulkEditForm default_return_url = 'dcim:cable_list' @@ -1870,7 +2102,7 @@ class CableBulkEditView(PermissionRequiredMixin, BulkEditView): class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_cable' queryset = Cable.objects.prefetch_related('termination_a', 'termination_b') - filter = filters.CableFilter + filterset = filters.CableFilterSet table = tables.CableTable default_return_url = 'dcim:cable_list' @@ -1888,8 +2120,8 @@ class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView): ).order_by( 'cable', 'connected_endpoint__device__name', 'connected_endpoint__name' ) - filter = filters.ConsoleConnectionFilter - filter_form = forms.ConsoleConnectionFilterForm + filterset = filters.ConsoleConnectionFilterSet + filterset_form = forms.ConsoleConnectionFilterForm table = tables.ConsoleConnectionTable template_name = 'dcim/console_connections_list.html' @@ -1907,7 +2139,8 @@ class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView): obj.get_connection_status_display(), ]) csv_data.append(csv) - return csv_data + + return '\n'.join(csv_data) class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView): @@ -1919,8 +2152,8 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView): ).order_by( 'cable', '_connected_poweroutlet__device__name', '_connected_poweroutlet__name' ) - filter = filters.PowerConnectionFilter - filter_form = forms.PowerConnectionFilterForm + filterset = filters.PowerConnectionFilterSet + filterset_form = forms.PowerConnectionFilterForm table = tables.PowerConnectionTable template_name = 'dcim/power_connections_list.html' @@ -1938,7 +2171,8 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView): obj.get_connection_status_display(), ]) csv_data.append(csv) - return csv_data + + return '\n'.join(csv_data) class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): @@ -1952,8 +2186,8 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): ).order_by( 'device' ) - filter = filters.InterfaceConnectionFilter - filter_form = forms.InterfaceConnectionFilterForm + filterset = filters.InterfaceConnectionFilterSet + filterset_form = forms.InterfaceConnectionFilterForm table = tables.InterfaceConnectionTable template_name = 'dcim/interface_connections_list.html' @@ -1977,7 +2211,8 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): obj.get_connection_status_display(), ]) csv_data.append(csv) - return csv_data + + return '\n'.join(csv_data) # @@ -1987,10 +2222,10 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): class InventoryItemListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_inventoryitem' queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') - filter = filters.InventoryItemFilter - filter_form = forms.InventoryItemFilterForm + filterset = filters.InventoryItemFilterSet + filterset_form = forms.InventoryItemFilterForm table = tables.InventoryItemTable - template_name = 'dcim/inventoryitem_list.html' + action_buttons = ('import', 'export') class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView): @@ -1998,13 +2233,13 @@ class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView): model = InventoryItem model_form = forms.InventoryItemForm - def alter_obj(self, obj, request, url_args, url_kwargs): - if 'device' in url_kwargs: - obj.device = get_object_or_404(Device, pk=url_kwargs['device']) - return obj - def get_return_url(self, request, obj): - return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk}) +class InventoryItemCreateView(PermissionRequiredMixin, ComponentCreateView): + permission_required = 'dcim.add_inventoryitem' + model = InventoryItem + form = forms.InventoryItemCreateForm + model_form = forms.InventoryItemForm + template_name = 'dcim/device_component_add.html' class InventoryItemDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -2022,7 +2257,7 @@ class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView): class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_inventoryitem' queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') - filter = filters.InventoryItemFilter + filterset = filters.InventoryItemFilterSet table = tables.InventoryItemTable form = forms.InventoryItemBulkEditForm default_return_url = 'dcim:inventoryitem_list' @@ -2044,9 +2279,9 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_virtualchassis' queryset = VirtualChassis.objects.prefetch_related('master').annotate(member_count=Count('members')) table = tables.VirtualChassisTable - filter = filters.VirtualChassisFilter - filter_form = forms.VirtualChassisFilterForm - template_name = 'dcim/virtualchassis_list.html' + filterset = filters.VirtualChassisFilterSet + filterset_form = forms.VirtualChassisFilterForm + action_buttons = ('export',) class VirtualChassisCreateView(PermissionRequiredMixin, View): @@ -2287,10 +2522,9 @@ class PowerPanelListView(PermissionRequiredMixin, ObjectListView): ).annotate( powerfeed_count=Count('powerfeeds') ) - filter = filters.PowerPanelFilter - filter_form = forms.PowerPanelFilterForm + filterset = filters.PowerPanelFilterSet + filterset_form = forms.PowerPanelFilterForm table = tables.PowerPanelTable - template_name = 'dcim/powerpanel_list.html' class PowerPanelView(PermissionRequiredMixin, View): @@ -2342,7 +2576,7 @@ class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): ).annotate( rack_count=Count('powerfeeds') ) - filter = filters.PowerPanelFilter + filterset = filters.PowerPanelFilterSet table = tables.PowerPanelTable default_return_url = 'dcim:powerpanel_list' @@ -2356,10 +2590,9 @@ class PowerFeedListView(PermissionRequiredMixin, ObjectListView): queryset = PowerFeed.objects.prefetch_related( 'power_panel', 'rack' ) - filter = filters.PowerFeedFilter - filter_form = forms.PowerFeedFilterForm + filterset = filters.PowerFeedFilterSet + filterset_form = forms.PowerFeedFilterForm table = tables.PowerFeedTable - template_name = 'dcim/powerfeed_list.html' class PowerFeedView(PermissionRequiredMixin, View): @@ -2402,7 +2635,7 @@ class PowerFeedBulkImportView(PermissionRequiredMixin, BulkImportView): class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_powerfeed' queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') - filter = filters.PowerFeedFilter + filterset = filters.PowerFeedFilterSet table = tables.PowerFeedTable form = forms.PowerFeedBulkEditForm default_return_url = 'dcim:powerfeed_list' @@ -2411,6 +2644,6 @@ class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView): class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerfeed' queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') - filter = filters.PowerFeedFilter + filterset = filters.PowerFeedFilterSet table = tables.PowerFeedTable default_return_url = 'dcim:powerfeed_list' diff --git a/netbox/extras/__init__.py b/netbox/extras/__init__.py index c7e9c66ad..3db5f9c25 100644 --- a/netbox/extras/__init__.py +++ b/netbox/extras/__init__.py @@ -1,15 +1 @@ -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured - - default_app_config = 'extras.apps.ExtrasConfig' - -# check that django-rq is installed and we can connect to redis -if settings.WEBHOOKS_ENABLED: - try: - import django_rq - except ImportError: - raise ImproperlyConfigured( - "django-rq is not installed! You must install this package per " - "the documentation to use the webhook backend." - ) diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index ee21b4f5d..f66cc248f 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -3,9 +3,7 @@ from django.contrib import admin from netbox.admin import admin_site from utilities.forms import LaxURLField -from .models import ( - CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, TopologyMap, Webhook, -) +from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, Webhook from .reports import get_report @@ -28,7 +26,7 @@ class WebhookForm(forms.ModelForm): class Meta: model = Webhook - exclude = [] + exclude = () def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -40,13 +38,35 @@ class WebhookForm(forms.ModelForm): @admin.register(Webhook, site=admin_site) class WebhookAdmin(admin.ModelAdmin): list_display = [ - 'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', - 'type_delete', 'ssl_verification', + 'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete', + 'ssl_verification', ] list_filter = [ 'enabled', 'type_create', 'type_update', 'type_delete', 'obj_type', ] form = WebhookForm + fieldsets = ( + (None, { + 'fields': ( + 'name', 'obj_type', 'enabled', + ) + }), + ('Events', { + 'fields': ( + 'type_create', 'type_update', 'type_delete', + ) + }), + ('HTTP Request', { + 'fields': ( + 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', + ) + }), + ('SSL', { + 'fields': ( + 'ssl_verification', 'ca_file_path', + ) + }) + ) def models(self, obj): return ', '.join([ct.name for ct in obj.obj_type.all()]) @@ -133,10 +153,10 @@ class CustomLinkAdmin(admin.ModelAdmin): @admin.register(Graph, site=admin_site) class GraphAdmin(admin.ModelAdmin): list_display = [ - 'name', 'type', 'weight', 'source', + 'name', 'type', 'weight', 'template_language', 'source', ] list_filter = [ - 'type', + 'type', 'template_language', ] @@ -197,15 +217,3 @@ class ReportResultAdmin(admin.ModelAdmin): def passing(self, obj): return not obj.failed passing.boolean = True - - -# -# Topology maps -# - -@admin.register(TopologyMap, site=admin_site) -class TopologyMapAdmin(admin.ModelAdmin): - list_display = ['name', 'slug', 'site'] - prepopulated_fields = { - 'slug': ['name'], - } diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index e0c70efa3..9a3041238 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -5,7 +5,7 @@ from django.db import transaction from rest_framework import serializers from rest_framework.exceptions import ValidationError -from extras.constants import * +from extras.choices import * from extras.models import CustomField, CustomFieldChoice, CustomFieldValue from utilities.api import ValidatedModelSerializer @@ -39,7 +39,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer): if value not in [None, '']: # Validate integer - if cf.type == CF_TYPE_INTEGER: + if cf.type == CustomFieldTypeChoices.TYPE_INTEGER: try: int(value) except ValueError: @@ -48,13 +48,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer): ) # Validate boolean - if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]: + if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]: raise ValidationError( "Invalid value for boolean field {}: {}".format(field_name, value) ) # Validate date - if cf.type == CF_TYPE_DATE: + if cf.type == CustomFieldTypeChoices.TYPE_DATE: try: datetime.strptime(value, '%Y-%m-%d') except ValueError: @@ -63,7 +63,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer): ) # Validate selected choice - if cf.type == CF_TYPE_SELECT: + if cf.type == CustomFieldTypeChoices.TYPE_SELECT: try: value = int(value) except ValueError: @@ -102,7 +102,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer): instance.custom_fields = {} for field in fields: value = instance.cf.get(field.name) - if field.type == CF_TYPE_SELECT and value is not None: + if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None: instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data else: instance.custom_fields[field.name] = value @@ -134,9 +134,9 @@ class CustomFieldModelSerializer(ValidatedModelSerializer): # Populate initial data using custom field default values for field in fields: if field.name not in self.initial_data['custom_fields'] and field.default: - if field.type == CF_TYPE_SELECT: + if field.type == CustomFieldTypeChoices.TYPE_SELECT: field_value = field.choices.get(value=field.default).pk - elif field.type == CF_TYPE_BOOLEAN: + elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN: field_value = bool(field.default) else: field_value = field.default diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 8cbddc860..40606ed8e 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -8,10 +8,10 @@ from dcim.api.nested_serializers import ( NestedRegionSerializer, NestedSiteSerializer, ) from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site +from extras.choices import * from extras.constants import * from extras.models import ( - ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, - Tag + ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ) from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.models import Tenant, TenantGroup @@ -20,6 +20,8 @@ from utilities.api import ( ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField, ValidatedModelSerializer, ) +from virtualization.api.nested_serializers import NestedClusterGroupSerializer, NestedClusterSerializer +from virtualization.models import Cluster, ClusterGroup from .nested_serializers import * @@ -28,17 +30,25 @@ from .nested_serializers import * # class GraphSerializer(ValidatedModelSerializer): - type = ChoiceField(choices=GRAPH_TYPE_CHOICES) + type = ContentTypeField( + queryset=ContentType.objects.filter(GRAPH_MODELS), + ) class Meta: model = Graph - fields = ['id', 'type', 'weight', 'name', 'source', 'link'] + fields = ['id', 'type', 'weight', 'name', 'template_language', 'source', 'link'] class RenderedGraphSerializer(serializers.ModelSerializer): - embed_url = serializers.SerializerMethodField() - embed_link = serializers.SerializerMethodField() - type = ChoiceField(choices=GRAPH_TYPE_CHOICES) + embed_url = serializers.SerializerMethodField( + read_only=True + ) + embed_link = serializers.SerializerMethodField( + read_only=True + ) + type = ContentTypeField( + read_only=True + ) class Meta: model = Graph @@ -56,9 +66,12 @@ class RenderedGraphSerializer(serializers.ModelSerializer): # class ExportTemplateSerializer(ValidatedModelSerializer): + content_type = ContentTypeField( + queryset=ContentType.objects.filter(EXPORTTEMPLATE_MODELS), + ) template_language = ChoiceField( - choices=TEMPLATE_LANGUAGE_CHOICES, - default=TEMPLATE_LANGUAGE_JINJA2 + choices=TemplateLanguageChoices, + default=TemplateLanguageChoices.LANGUAGE_JINJA2 ) class Meta: @@ -69,18 +82,6 @@ class ExportTemplateSerializer(ValidatedModelSerializer): ] -# -# Topology maps -# - -class TopologyMapSerializer(ValidatedModelSerializer): - site = NestedSiteSerializer() - - class Meta: - model = TopologyMap - fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description'] - - # # Tags # @@ -169,6 +170,18 @@ class ConfigContextSerializer(ValidatedModelSerializer): required=False, many=True ) + cluster_groups = SerializedPKRelatedField( + queryset=ClusterGroup.objects.all(), + serializer=NestedClusterGroupSerializer, + required=False, + many=True + ) + clusters = SerializedPKRelatedField( + queryset=Cluster.objects.all(), + serializer=NestedClusterSerializer, + required=False, + many=True + ) tenant_groups = SerializedPKRelatedField( queryset=TenantGroup.objects.all(), serializer=NestedTenantGroupSerializer, @@ -181,12 +194,18 @@ class ConfigContextSerializer(ValidatedModelSerializer): required=False, many=True ) + tags = serializers.SlugRelatedField( + queryset=Tag.objects.all(), + slug_field='slug', + required=False, + many=True + ) class Meta: model = ConfigContext fields = [ 'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', - 'tenant_groups', 'tenants', 'data', + 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', ] @@ -213,6 +232,52 @@ class ReportDetailSerializer(ReportSerializer): result = ReportResultSerializer() +# +# Scripts +# + +class ScriptSerializer(serializers.Serializer): + id = serializers.SerializerMethodField(read_only=True) + name = serializers.SerializerMethodField(read_only=True) + description = serializers.SerializerMethodField(read_only=True) + vars = serializers.SerializerMethodField(read_only=True) + + def get_id(self, instance): + return '{}.{}'.format(instance.__module__, instance.__name__) + + def get_name(self, instance): + return getattr(instance.Meta, 'name', instance.__name__) + + def get_description(self, instance): + return getattr(instance.Meta, 'description', '') + + def get_vars(self, instance): + return { + k: v.__class__.__name__ for k, v in instance._get_vars().items() + } + + +class ScriptInputSerializer(serializers.Serializer): + data = serializers.JSONField() + commit = serializers.BooleanField() + + +class ScriptLogMessageSerializer(serializers.Serializer): + status = serializers.SerializerMethodField(read_only=True) + message = serializers.SerializerMethodField(read_only=True) + + def get_status(self, instance): + return LOG_LEVEL_CODES.get(instance[0]) + + def get_message(self, instance): + return instance[1] + + +class ScriptOutputSerializer(serializers.Serializer): + log = ScriptLogMessageSerializer(many=True, read_only=True) + output = serializers.CharField(read_only=True) + + # # Change logging # @@ -222,7 +287,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer): read_only=True ) action = ChoiceField( - choices=OBJECTCHANGE_ACTION_CHOICES, + choices=ObjectChangeActionChoices, read_only=True ) changed_object_type = ContentTypeField( diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index f4968d004..d699cd22e 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -15,34 +15,34 @@ router = routers.DefaultRouter() router.APIRootView = ExtrasRootView # Field choices -router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice') +router.register('_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice') # Custom field choices -router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice') +router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice') # Graphs -router.register(r'graphs', views.GraphViewSet) +router.register('graphs', views.GraphViewSet) # Export templates -router.register(r'export-templates', views.ExportTemplateViewSet) - -# Topology maps -router.register(r'topology-maps', views.TopologyMapViewSet) +router.register('export-templates', views.ExportTemplateViewSet) # Tags -router.register(r'tags', views.TagViewSet) +router.register('tags', views.TagViewSet) # Image attachments -router.register(r'image-attachments', views.ImageAttachmentViewSet) +router.register('image-attachments', views.ImageAttachmentViewSet) # Config contexts -router.register(r'config-contexts', views.ConfigContextViewSet) +router.register('config-contexts', views.ConfigContextViewSet) # Reports -router.register(r'reports', views.ReportViewSet, basename='report') +router.register('reports', views.ReportViewSet, basename='report') + +# Scripts +router.register('scripts', views.ScriptViewSet, basename='script') # Change logging -router.register(r'object-changes', views.ObjectChangeViewSet) +router.register('object-changes', views.ObjectChangeViewSet) app_name = 'extras-api' urlpatterns = router.urls diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 526db20a2..167768861 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -2,8 +2,8 @@ from collections import OrderedDict from django.contrib.contenttypes.models import ContentType from django.db.models import Count -from django.http import Http404, HttpResponse -from django.shortcuts import get_object_or_404 +from django.http import Http404 +from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response @@ -11,10 +11,10 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet from extras import filters from extras.models import ( - ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, - Tag, + ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ) from extras.reports import get_report, get_reports +from extras.scripts import get_script, get_scripts from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet from . import serializers @@ -25,9 +25,9 @@ from . import serializers class ExtrasFieldChoicesViewSet(FieldChoicesViewSet): fields = ( - (ExportTemplate, ['template_language']), - (Graph, ['type']), - (ObjectChange, ['action']), + (serializers.ExportTemplateSerializer, ['template_language']), + (serializers.GraphSerializer, ['type', 'template_language']), + (serializers.ObjectChangeSerializer, ['action']), ) @@ -102,7 +102,7 @@ class CustomFieldModelViewSet(ModelViewSet): class GraphViewSet(ModelViewSet): queryset = Graph.objects.all() serializer_class = serializers.GraphSerializer - filterset_class = filters.GraphFilter + filterset_class = filters.GraphFilterSet # @@ -112,35 +112,7 @@ class GraphViewSet(ModelViewSet): class ExportTemplateViewSet(ModelViewSet): queryset = ExportTemplate.objects.all() serializer_class = serializers.ExportTemplateSerializer - filterset_class = filters.ExportTemplateFilter - - -# -# Topology maps -# - -class TopologyMapViewSet(ModelViewSet): - queryset = TopologyMap.objects.prefetch_related('site') - serializer_class = serializers.TopologyMapSerializer - filterset_class = filters.TopologyMapFilter - - @action(detail=True) - def render(self, request, pk): - - tmap = get_object_or_404(TopologyMap, pk=pk) - img_format = 'png' - - try: - data = tmap.render(img_format=img_format) - except Exception as e: - return HttpResponse( - "There was an error generating the requested graph: %s" % e - ) - - response = HttpResponse(data, content_type='image/{}'.format(img_format)) - response['Content-Disposition'] = 'inline; filename="{}.{}"'.format(tmap.slug, img_format) - - return response + filterset_class = filters.ExportTemplateFilterSet # @@ -152,7 +124,7 @@ class TagViewSet(ModelViewSet): tagged_items=Count('extras_taggeditem_items', distinct=True) ) serializer_class = serializers.TagSerializer - filterset_class = filters.TagFilter + filterset_class = filters.TagFilterSet # @@ -173,7 +145,7 @@ class ConfigContextViewSet(ModelViewSet): 'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants', ) serializer_class = serializers.ConfigContextSerializer - filterset_class = filters.ConfigContextFilter + filterset_class = filters.ConfigContextFilterSet # @@ -252,6 +224,56 @@ class ReportViewSet(ViewSet): return Response(serializer.data) +# +# Scripts +# + +class ScriptViewSet(ViewSet): + permission_classes = [IsAuthenticatedOrLoginNotRequired] + _ignore_model_permissions = True + exclude_from_schema = True + lookup_value_regex = '[^/]+' # Allow dots + + def _get_script(self, pk): + module_name, script_name = pk.split('.') + script = get_script(module_name, script_name) + if script is None: + raise Http404 + return script + + def list(self, request): + + flat_list = [] + for script_list in get_scripts().values(): + flat_list.extend(script_list.values()) + + serializer = serializers.ScriptSerializer(flat_list, many=True, context={'request': request}) + + return Response(serializer.data) + + def retrieve(self, request, pk): + script = self._get_script(pk) + serializer = serializers.ScriptSerializer(script, context={'request': request}) + + return Response(serializer.data) + + def post(self, request, pk): + """ + Run a Script identified as ". +``` + +>>> Immediately following a new release, it takes some time for CDNs to catch up and get the new versions live on the CDN. + +## Installing with Bower + +Select2 is available on Bower. Add the following to your `bower.json` file and then run `bower install`: + +``` +"dependencies": { + "select2": "~4.0" +} +``` + +Or, run `bower install select2` from your project directory. + +The precompiled distribution files will be available in `vendor/select2/dist/css/` and `vendor/select2/dist/js/`, relative to your project directory. Include them in your page: + +``` + + +``` + +## Manual installation + +We strongly recommend that you use either a CDN or a package manager like Bower or npm. This will make it easier for you to deploy your project in different environments, and easily update Select2 when new versions are released. Nonetheless if you prefer to integrate Select2 into your project manually, you can [download the release of your choice](https://github.com/select2/select2/tags) from GitHub and copy the files from the `dist` directory into your project. + +Include the compiled files in your page: + +``` + + +``` diff --git a/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/02.basic-usage/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/02.basic-usage/docs.md new file mode 100644 index 000000000..e19c98f5e --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/02.basic-usage/docs.md @@ -0,0 +1,106 @@ +--- +title: Basic usage +taxonomy: + category: docs +process: + twig: true +never_cache_twig: true +--- + +## Single select boxes + +Select2 was designed to be a replacement for the standard ` + +and turn it into this... + +
+ +
+ +``` + +``` + + + +Select2 will register itself as a jQuery function if you use any of the distribution builds, so you can call `.select2()` on any jQuery selector where you would like to initialize Select2. + +``` +// In your Javascript (external .js resource or diff --git a/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/03.builds-and-modules/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/03.builds-and-modules/docs.md new file mode 100644 index 000000000..4bcf65290 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/03.builds-and-modules/docs.md @@ -0,0 +1,69 @@ +--- +title: Builds and modules +taxonomy: + category: docs +process: + twig: true +--- + +## The different Select2 builds + +Select2 provides multiple builds that are tailored to different +environments where it is going to be used. If you think you need to use +Select2 in a nonstandard environment, like when you are using AMD, you +should read over the list below. + + + + + + + + + + + + + + + + + + +
Build nameWhen you should use it
+ Standard (select2.js / select2.min.js) + + This is the build that most people should be using for Select2. It + includes the most commonly used features. +
+ Full (select2.full.js / select2.full.min.js) + + You should only use this build if you need the additional features from Select2, like the compatibility modules or recommended includes like jquery.mousewheel +
+ +## Using Select2 with AMD or CommonJS loaders + +Select2 should work with most AMD- or CommonJS-compliant module loaders, including [RequireJS](http://requirejs.org/) and [almond](https://github.com/jrburke/almond). Select2 ships with a modified version of the [UMD jQuery template](https://github.com/umdjs/umd/blob/f208d385768ed30cd0025d5415997075345cd1c0/templates/jqueryPlugin.js) that supports both CommonJS and AMD environments. + +### Configuration + +For most AMD and CommonJS setups, the location of the data files used by Select2 will be automatically determined and handled without you needing to do anything. + +Select2 internally uses AMD and the r.js build tool to build the files located in the `dist` folder. These are built using the files in the `src` folder, so _you can_ just point your modules to the Select2 source and load in `jquery.select2` or `select2/core` when you want to use Select2. The files located in the `dist` folder are also AMD-compatible, so you can point to that file if you want to load in all of the default Select2 modules. + +If you are using Select2 in a build environment where preexisting module names are changed during a build step, Select2 may not be able to find optional dependencies or language files. You can manually set the prefixes to use for these files using the `amdBase` and `amdLanguageBase` options. + +``` +$.fn.select2.defaults.set('amdBase', 'select2/'); +$.fn.select2.defaults.set('amdLanguageBase', 'select2/i18n/'); +``` + +#### `amdBase` + +Specifies the base AMD loader path to be used for select2 dependency resolution. This option typically doesn't need to be changed, but is available for situations where module names may change as a result of certain build environments. + +#### `amdLanguageBase` + +Specifies the base AMD loader language path to be used for select2 language file resolution. This option typically doesn't need to be changed, but is available for situations where module names may change as a result of certain build environments. + +>>> Due to [a bug in older versions](https://github.com/jrburke/requirejs/issues/1342) of the r.js build tool, Select2 was sometimes placed before jQuery in then compiled build file. Because of this, Select2 will trigger an error because it won't be able to find or load jQuery. By upgrading to version 2.1.18 or higher of the r.js build tool, you will be able to fix the issue. diff --git a/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/chapter.md b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/chapter.md new file mode 100644 index 000000000..a09a28e3b --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/chapter.md @@ -0,0 +1,84 @@ +--- +title: Getting Started +taxonomy: + category: docs +process: + twig: true +twig_first: true +--- + +![Select2 logo](/images/logo.png) + +# Select2 + +The jQuery replacement for select boxes + + + +Select2 gives you a customizable select box with support for searching, tagging, remote data sets, infinite scrolling, and many other highly used options. + +
+
+
+ +

In your language

+

+ Select2 comes with support for + RTL environments, + searching with diacritics and + over 40 languages built-in. +

+
+ +
+ +

Remote data support

+

+ Using AJAX you can efficiently + search large lists of items. +

+
+ +
+ +

Theming

+

+ Fully skinnable, CSS built with Sass and an + optional theme for Bootstrap 3. +

+
+
+ +
+
+ +

Fully extensible

+

+ The plugin system + allows you to easily customize Select2 to work exactly how you want it + to. +

+
+ +
+ +

Dynamic item creation

+

+ Allow users to type in a new option and + add it on the fly. +

+
+ +
+ +

Full browser support

+

Support for both modern and legacy browsers is built-in, even including Internet Explorer 8.

+
+
+
+ +>>>>>

Looking for the documentation for Select2 3.5.3? That can still be found here.

diff --git a/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/01.getting-help/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/01.getting-help/docs.md new file mode 100644 index 000000000..32c6b6773 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/01.getting-help/docs.md @@ -0,0 +1,39 @@ +--- +title: Getting Help +metadata: + description: How to get support, report a bug, or suggest a feature for Select2. +taxonomy: + category: docs +--- + +## General support + +Having trouble getting Select2 working on your website? Is it not working together with another plugin, even though you think it should? Select2 has a few communities that you can go to for help getting it all working together. + +1. Join our [forums](https://forums.select2.org), graciously hosted by [NextGI](https://nextgi.com) and start a new topic. +2. Search [Stack Overflow](http://stackoverflow.com/questions/tagged/jquery-select2?sort=votes) **carefully** for existing questions that might address your issue. If you need to open a new question, make sure to include the `jquery-select2` tag. +3. Ask in the `#select2` channel on `chat.freenode.net` or use the [web irc client.](https://webchat.freenode.net/?channels=select2) + +>>>> Do **NOT** use the GitHub issue tracker for general support and troubleshooting questions. The issue tracker is **only** for bug reports with a [minimal, complete, and verifiable example](https://stackoverflow.com/help/mcve) and feature requests. Use the forums instead. + +## Reporting bugs + +Found a problem with Select2? Feel free to open a ticket on the Select2 repository on GitHub, but you should keep a few things in mind: + +1. Use the [GitHub issue search](https://github.com/select2/select2/search?q=&type=Issues) to check if your issue has already been reported. +2. Try to isolate your problem as much as possible. Use [JS Bin](http://jsbin.com/goqagokoye/edit?html,js,output) to create a [minimal, verifiable, and complete](https://stackoverflow.com/help/mcve) example of the problem. +3. Once you are sure the issue is with Select2, and not a third party library, [open an issue](https://github.com/select2/select2/issues/new) with a description of the bug, and link to your jsbin example. + +You can find more information on reporting bugs in the [contributing guide,](https://github.com/select2/select2/blob/master/CONTRIBUTING.md#reporting-bugs-with-select2) including tips on what information to include. + +>>>>> If you are not conversationally proficient in English, do **NOT** post a machine translation (e.g. Google Translate) to GitHub. Get help in crafting your question, either via the [forums](https://forums.select2.org) or in [chat](https://webchat.freenode.net/?channels=select2). If all else fails, you may post your bug report or feature request in your native language and we will tag it with `translation-needed` so that it can be properly translated. + +## Requesting new features + +New feature requests are usually requested by the [Select2 community on GitHub,](https://github.com/select2/select2/issues) and are often fulfilled by [fellow contributors.](https://github.com/select2/select2/blob/master/CONTRIBUTING.md) + +1. Use the [GitHub issue search](https://github.com/select2/select2/search?q=&type=Issues) to check if your feature has already been requested. +2. Check if it hasn't already been implemented as a [third party plugin.](https://github.com/search?q=topic%3Aselect2&type=Repositories) +3. Please make sure you are only requesting a single feature, and not a collection of smaller features. + +You can find more information on requesting new features in the [contributing guide.](https://github.com/select2/select2/blob/master/CONTRIBUTING.md#requesting-features-in-select2) diff --git a/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/02.common-problems/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/02.common-problems/docs.md new file mode 100644 index 000000000..85329454b --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/02.common-problems/docs.md @@ -0,0 +1,48 @@ +--- +title: Common problems +metadata: + description: Commonly encountered issues when using Select2. +taxonomy: + category: docs +--- + +### Select2 does not function properly when I use it inside a Bootstrap modal. + +This issue occurs because Bootstrap modals tend to steal focus from other elements outside of the modal. Since by default, Select2 [attaches the dropdown menu to the `` element](/dropdown#dropdown-placement), it is considered "outside of the modal". + +To avoid this problem, you may attach the dropdown to the modal itself with the [dropdownParent](/dropdown#dropdown-placement) setting: + +``` + + +... + + +``` + +This will cause the dropdown to be attached to the modal, rather than the `` element. + +**Alternatively**, you may simply globally override Bootstrap's behavior: + +``` +// Do this before you initialize any of your modals +$.fn.modal.Constructor.prototype.enforceFocus = function() {}; +``` + +See [this answer](https://stackoverflow.com/questions/18487056/select2-doesnt-work-when-embedded-in-a-bootstrap-modal/19574076#19574076) for more information. + +### The dropdown becomes misaligned/displaced when using pinch-zoom. + +See [#5048](https://github.com/select2/select2/issues/5048). The problem is that some browsers' implementations of pinch-zoom affect the `body` element, which [Select2 attaches to by default](https://select2.org/dropdown#dropdown-placement), causing it to render incorrectly. + +The solution is to use `dropdownParent` to attach the dropdown to a more specific element. diff --git a/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/chapter.md b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/chapter.md new file mode 100644 index 000000000..e4c9f5185 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/chapter.md @@ -0,0 +1,11 @@ +--- +title: Troubleshooting +metadata: + description: The chapter covers some common issues you may encounter with Select2, as well as where you can go to get help. +taxonomy: + category: docs +--- + +# Troubleshooting + +The chapter covers some common issues you may encounter with Select2, as well as where you can go to get help. \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/01.options-api/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/01.options-api/docs.md new file mode 100644 index 000000000..06af83c7d --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/01.options-api/docs.md @@ -0,0 +1,52 @@ +--- +title: Options +taxonomy: + category: docs +--- + +This is a list of all the Select 2 configuration options. + +| Option | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| `adaptContainerCssClass` | | | | +| `adaptDropdownCssClass` | | | | +| `ajax` | object | `null` | Provides support for [ajax data sources](/data-sources/ajax). | +| `allowClear` | boolean | `false` | Provides support for [clearable selections](/selections#clearable-selections). | +| `amdBase` | string | `./` | See [Using Select2 with AMD or CommonJS loaders](/builds-and-modules#using-select2-with-amd-or-commonjs-loaders). | +| `amdLanguageBase` | string | `./i18n/` | See [Using Select2 with AMD or CommonJS loaders](/builds-and-modules#using-select2-with-amd-or-commonjs-loaders). | +| `closeOnSelect` | boolean | `true` | Controls whether the dropdown is [closed after a selection is made](/dropdown#forcing-the-dropdown-to-remain-open-after-selection). | +| `containerCss` | object | null | Adds custom CSS to the container. Expects key-value pairs: `{ 'css-property': 'value' }` | +| `containerCssClass` | string | `''` | | +| `data` | array of objects | `null` | Allows rendering dropdown options from an [array](/data-sources/arrays). | +| `dataAdapter` | | `SelectAdapter` | Used to override the built-in [DataAdapter](/advanced/default-adapters/data). | +| `debug` | boolean | `false` | Enable debugging messages in the browser console. | +| `dir` | | | +| `disabled` | boolean | `false` | When set to `true`, the select control will be disabled. | +| `dropdownAdapter` | | `DropdownAdapter` | Used to override the built-in [DropdownAdapter](/advanced/default-adapters/dropdown) | +| `dropdownAutoWidth` | boolean | `false` | | +| `dropdownCss` | object | null | Adds custom CSS to the dropdown. Expects key-value pairs: `{ 'css-property': 'value' }` | +| `dropdownCssClass` | string | `''` | | +| `dropdownParent` | jQuery selector or DOM node | `$(document.body)` | Allows you to [customize placement](/dropdown#dropdown-placement) of the dropdown. | +| `escapeMarkup` | callback | `Utils.escapeMarkup` | Handles [automatic escaping of content rendered by custom templates](/dropdown#built-in-escaping). | +| `initSelection` | callback | | See [`initSelection`](/upgrading/migrating-from-35#removed-the-requirement-of-initselection). **This option was deprecated in Select2 v4.0, and will be removed in v4.1.** | +| `language` | string or object | `EnglishTranslation` | Specify the [language used for Select2 messages](/i18n#message-translations). | +| `matcher` | A callback taking search `params` and the `data` object. | | Handles custom [search matching](/searching#customizing-how-results-are-matched). | +| `maximumInputLength` | integer | `0` | [Maximum number of characters](/searching#maximum-search-term-length) that may be provided for a search term. | +| `maximumSelectionLength` | integer | `0` | The maximum number of items that may be selected in a multi-select control. If the value of this option is less than 1, the number of selected items will not be limited. +| `minimumInputLength` | integer | `0` | [Minimum number of characters required to start a search.](/searching#minimum-search-term-length) | +| `minimumResultsForSearch` | integer | `0` | The minimum number of results required to [display the search box](/searching#limiting-display-of-the-search-box-to-large-result-sets). | +| `multiple` | boolean | `false` | This option enables multi-select (pillbox) mode. Select2 will automatically map the value of the `multiple` HTML attribute to this option during initialization. | +| `placeholder` | string or object | `null` | Specifies the [placeholder](/placeholders) for the control. | +| `query` | A function taking `params` (including a `callback`) | `Query` | **This option was deprecated in Select2 v4.0, and will be removed in v4.1.** | +| `resultsAdapter` | | `ResultsAdapter` | Used to override the built-in [ResultsAdapter](/advanced/default-adapters/results). | +| `selectionAdapter` | | `SingleSelection` or `MultipleSelection`, depending on the value of `multiple`. | Used to override the built-in [SelectionAdapter](/advanced/default-adapters/selection). | +| `selectOnClose` | boolean | `false` | Implements [automatic selection](/dropdown#automatic-selection) when the dropdown is closed. | +| `sorter` | callback | | | +| `tags` | boolean / array of objects | `false` | Used to enable [free text responses](/tagging). | +| `templateResult` | callback | | Customizes the way that [search results are rendered](/dropdown#templating). | +| `templateSelection` | callback | | Customizes the way that [selections are rendered](/selections#templating). | +| `theme` | string | `default` | Allows you to set the [theme](/appearance#themes). | +| `tokenizer` | callback | | A callback that handles [automatic tokenization of free-text entry](/tagging#automatic-tokenization-into-tags). | +| `tokenSeparators` | array | `[]` | The list of characters that should be used as token separators. | +| `width` | string | `resolve` | Supports [customization of the container width](/appearance#container-width). | +| `scrollAfterSelect` | boolean | `false` | If `true`, resolves issue for multiselects using `closeOnSelect: false` that caused the list of results to scroll to the first selection after each select/unselect (see https://github.com/select2/select2/pull/5150). This behaviour was intentional to deal with infinite scroll UI issues (if you need this behavior, set `false`) but it created an issue with multiselect dropdown boxes of fixed length. This pull request adds a configurable option to toggle between these two desirable behaviours. | diff --git a/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/02.defaults/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/02.defaults/docs.md new file mode 100644 index 000000000..a5a9a6355 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/02.defaults/docs.md @@ -0,0 +1,31 @@ +--- +title: Global defaults +taxonomy: + category: docs +--- + +In some cases, you need to set the default options for all instances of Select2 in your web application. This is especially useful when you are migrating from past versions of Select2, or you are using non-standard options like [custom AMD builds](/getting-started/builds-and-modules#using-select2-with-amd-or-commonjs-loaders). Select2 exposes the default options through `$.fn.select2.defaults`, which allows you to set them globally. + +When setting options globally, any past defaults that have been set will be overridden. Default options are only used when an option is requested that has not been set during initialization. + +You can set default options by calling `$.fn.select2.defaults.set("key", "value")`. For example: + +``` +$.fn.select2.defaults.set("theme", "classic"); +``` + +## Nested options + +To set a default values for cache, use the same notation used for [HTML `data-*` attributes](/configuration/data-attributes). Two dashes (`--`) will be replaced by a level of nesting, and a single dash (`-`) will convert the key to a camelCase string: + +``` +$.fn.select2.defaults.set("ajax--cache", false); +``` + +## Resetting default options + +You can reset the default options to their initial values by calling + +``` +$.fn.select2.defaults.reset(); +``` diff --git a/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/03.data-attributes/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/03.data-attributes/docs.md new file mode 100644 index 000000000..7f4c7e8cf --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/03.data-attributes/docs.md @@ -0,0 +1,64 @@ +--- +title: data-* attributes +taxonomy: + category: docs +--- + +It is recommended that you declare your configuration options by [passing in an object](/configuration) when initializing Select2. However, you may also define your configuration options by using the HTML5 `data-*` attributes, which will override any options set when initializing Select2 and any [defaults](/configuration/defaults). + +``` + +``` + +>>> Some options are not supported as `data-*`, for example `disabled` as it's not a Javascript option, but it's an HTML [attribute](/configuration/options-api). + +## Nested (subkey) options + +Sometimes, you have options that are nested under a top-level option. For example, the options under the `ajax` option: + +``` +$(".js-example-data-ajax").select2({ + ajax: { + url: "http://example.org/api/test", + cache: false + } +}); +``` + +To write these options as `data-*` attributes, each level of nesting should be separated by two dashes (`--`): + +``` + +``` + +The value of the option is subject to jQuery's [parsing rules](https://api.jquery.com/data/#data-html5) for HTML5 data attributes. + +>>> Due to [a jQuery bug](https://github.com/jquery/jquery/issues/2070), nested options using `data-*` attributes [do not work in jQuery 1.x](https://github.com/select2/select2/issues/2969). + +## `camelCase` options + +HTML data attributes are case-insensitive, so any options which contain capital letters will be parsed as if they were all lowercase. Because Select2 has many options which are camelCase, where words are separated by uppercase letters, you must write these options out with dashes instead. So an option that would normally be called `allowClear` should instead be defined as `allow-clear`. + +This means that declaring your ` + ... + +``` + +Will be interpreted the same as initializing Select2 as... + +``` +$("select").select2({ + tags: "true", + placeholder: "Select an option", + allowClear: true +}); +``` diff --git a/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/docs.md new file mode 100644 index 000000000..46db73eb6 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/docs.md @@ -0,0 +1,13 @@ +--- +title: Configuration +taxonomy: + category: docs +--- + +To configure custom options when you initialize Select2, simply pass an object in your call to `.select2()`: + +``` +$('.js-example-basic-single').select2({ + placeholder: 'Select an option' +}); +``` diff --git a/netbox/project-static/select2-4.0.12/docs/pages/04.appearance/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/04.appearance/docs.md new file mode 100644 index 000000000..c499d31dc --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/04.appearance/docs.md @@ -0,0 +1,216 @@ +--- +title: Appearance +taxonomy: + category: docs +process: + twig: true +never_cache_twig: true +--- + +The appearance of your Select2 controls can be customized via the standard HTML attributes for `` elements. You can also initialize Select2 with `disabled: true` to get the same effect. + +
+

+ +

+ +

+ +

+
+ + +
+
+ +

+
+
+
+## Labels
+
+You can, and should, use a `
+  

+

+ +

+ + +``` + + + +``` + + + +## Container width + +Select2 will try to match the width of the original element as closely as possible. Sometimes this isn't perfect, in which case you may manually set the `width` [configuration option](/configuration): + + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueDescription
'element' + Uses the computed element width from any applicable CSS rules. +
'style' + Width is determined from the select element's style attribute. If no style attribute is found, null is returned as the width. +
'resolve' + Uses the style attribute value if available, falling back to the computed element width as necessary. +
'<value>' + Valid CSS values can be passed as a string (e.g. width: '80%'). +
+ +### Example + +The two Select2 boxes below are styled to `50%` and `75%` width respectively to support responsive design: + +
+

+ +

+

+ +

+
+ +``` + + +``` + +

+
+
+
+>>>> Select2 will do its best to resolve the percent width specified via a CSS class, but it is not always possible. The best way to ensure that Select2 is using a percent based width is to inline the `style` declaration into the tag.
+
+## Themes
+
+Select2 supports custom themes using the `theme` option so you can style Select2 to match the rest of your application.
+
+These examples use the `classic` theme, which matches the old look of Select2.
+
+
+

+ +

+

+ +

+
+ +

+
+
+
+Various display options of the Select2 component can be changed.  You can access the ``) and any attributes on those elements using `.element`.
diff --git a/netbox/project-static/select2-4.0.12/docs/pages/05.options/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/05.options/docs.md
new file mode 100644
index 000000000..7b56c7111
--- /dev/null
+++ b/netbox/project-static/select2-4.0.12/docs/pages/05.options/docs.md
@@ -0,0 +1,81 @@
+---
+title: Options
+taxonomy:
+    category: docs
+process:
+    twig: true
+never_cache_twig: true
+---
+
+A traditional `` element that contains `` elements will be converted into data objects using the following rules:
+
+```
+{
+  "text": "label attribute",
+  "children": [ option data object, ... ],
+  "element": HTMLOptGroupElement
+}
+```
+
+>>> Options sourced from [other data sources](/data-sources) must conform to this this same internal representation.  See ["The Select2 data format"](/data-sources/formats) for details.
+
+## Dropdown option groups
+
+In HTML, `` element:
+
+```
+
+```
+
+Select2 will automatically pick these up and render them appropriately in the dropdown.
+
+### Hierarchical options
+
+Only a single level of nesting is allowed per the HTML specification. If you nest an `` within another ``, Select2 will not be able to detect the extra level of nesting and errors may be triggered as a result.
+
+Furthermore, `` elements **cannot** be made selectable.  This is a limitation of the HTML specification and is not a limitation that Select2 can overcome.
+
+If you wish to create a true hierarchy of selectable options, use an `` and [change the style with CSS](http://stackoverflow.com/q/30820215/359284#30948247).  Please note that this approach may be considered "less accessible" as it relies on CSS styling, rather than the semantic meaning of ``, to generate the effect.
+
+## Disabling options
+
+Select2 will correctly handle disabled options, both with data coming from a standard select (when the `disabled` attribute is set) and from remote sources, where the object has `disabled: true` set.
+
+
+ +
+ +

+
+```
+
+```
+
+
diff --git a/netbox/project-static/select2-4.0.12/docs/pages/06.data-sources/01.formats/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/06.data-sources/01.formats/docs.md
new file mode 100644
index 000000000..73d044b22
--- /dev/null
+++ b/netbox/project-static/select2-4.0.12/docs/pages/06.data-sources/01.formats/docs.md
@@ -0,0 +1,134 @@
+---
+title: The Select2 data format
+taxonomy:
+    category: docs
+---
+
+Select2 can render programmatically supplied data from an array or remote data source (AJAX) as dropdown options.  In order to accomplish this, Select2 expects a very specific data format.  This format consists of a JSON object containing an array of objects keyed by the `results` key.
+
+```
+{
+  "results": [
+    {
+      "id": 1,
+      "text": "Option 1"
+    },
+    {
+      "id": 2,
+      "text": "Option 2"
+    }
+  ],
+  "pagination": {
+    "more": true
+  }
+}
+```
+
+Select2 requires that each object contain an `id` and a `text` property.  Additional parameters passed in with data objects will be included on the data objects that Select2 exposes.
+
+The response object may also contain pagination data, if you would like to use the "infinite scroll" feature.  This should be specified under the `pagination` key.
+
+## Selected and disabled options
+
+You can also supply the `selected` and `disabled` properties for the options in this data structure.  For example:
+
+```
+{
+  "results": [
+    {
+      "id": 1,
+      "text": "Option 1"
+    },
+    {
+      "id": 2,
+      "text": "Option 2",
+      "selected": true
+    },
+    {
+      "id": 3,
+      "text": "Option 3",
+      "disabled": true
+    }
+  ]
+}
+```
+
+In this case, Option 2 will be pre-selected, and Option 3 will be [disabled](/options#disabling-options).
+
+## Transforming data into the required format
+
+### Generating `id` properties
+
+Select2 requires that the `id` property is used to uniquely identify the options that are displayed in the results list. If you use a property other than `id` (like `pk`) to uniquely identify an option, you need to map your old property to `id` before passing it to Select2.
+
+If you cannot do this on your server or you are in a situation where the API cannot be changed, you can do this in JavaScript before passing it to Select2:
+
+```
+var data = $.map(yourArrayData, function (obj) {
+  obj.id = obj.id || obj.pk; // replace pk with your identifier
+
+  return obj;
+});
+```
+
+### Generating `text` properties
+
+Just like with the `id` property, Select2 requires that the text that should be displayed for an option is stored in the `text` property. You can map this property from any existing property using the following JavaScript:
+
+```
+var data = $.map(yourArrayData, function (obj) {
+  obj.text = obj.text || obj.name; // replace name with the property used for the text
+
+  return obj;
+});
+```
+
+## Automatic string casting
+
+Because the `value` attribute on a `
+
+**In your HTML:**
+
+```
+
+```
+
+**In your Javascript:**
+
+```
+$('.js-data-example-ajax').select2({
+  ajax: {
+    url: 'https://api.github.com/search/repositories',
+    dataType: 'json'
+    // Additional AJAX parameters go here; see the end of this chapter for the full code of this example
+  }
+});
+```
+
+You can configure how Select2 searches for remote data using the `ajax` option.  Select2 will pass any options in the `ajax` object to jQuery's `$.ajax` function, or the `transport` function you specify.
+
+>>> For **remote data sources only**, Select2 does not create a new `
+    
+    
+    
+
+```
+
+The options that you create should have `selected="selected"` set so Select2 and the browser knows that they should be selected. The `value` attribute of the option should also be set to the value that will be returned from the server for the result, so Select2 can highlight it as selected in the dropdown. The text within the option should also reflect the value that should be displayed by default for the option.
+
+## Advanced matching of searches
+
+In past versions of Select2 the `matcher` callback processed options at every level, which limited the control that you had when displaying results, especially in cases where there was nested data. The `matcher` function was only given the individual option, even if it was a nested options, without any context.
+
+With the new [matcher function](/searching), only the root-level options are matched and matchers are expected to limit the results of any children options that they contain. This allows developers to customize how options within groups can be displayed, and modify how the results are returned.
+ 
+### Wrapper for old-style `matcher` callbacks
+
+For backwards compatibility, a wrapper function has been created that allows old-style matcher functions to be converted to the new style. 
+
+This wrapper function is only bundled in the [full version of Select2](/getting-started/builds-and-modules).  You can retrieve the function from the `select2/compat/matcher` module, which should just wrap the old matcher function.
+
+
+ +
+ +

+
+
+
+>>>> This will work for any matchers that only took in the search term and the text of the option as parameters. If your matcher relied on the third parameter containing the jQuery element representing the original `
+    
+
+```
+
+You would have previously had to get the placeholder option through the `placeholderOption`, but now you can do it through the `placeholder` option by setting an `id`.
+
+```
+$("select").select2({
+    placeholder: {
+        id: "-1",
+        placeholder: "Select an option"
+    }
+});
+```
+
+And Select2 will automatically display the placeholder when the value of the select is `-1`, which it will be by default. This does not break the old functionality of Select2 where the placeholder option was blank by default.
+
+## Display reflects the actual order of the values
+
+In past versions of Select2, choices were displayed in the order that they were selected. In cases where Select2 was used on a `
+```
+
+...then you should now declare it as...
+
+```
+
+```
+
+## Deprecated and removed methods
+
+As Select2 now uses a `` element instead. If you needed the second parameter (`triggerChange`), you should also call `.trigger("change")` on the element.
+
+```
+$("select").val("1").trigger("change"); // instead of $("select").select2("val", "1");
+```
+
+### `.select2("enable")`
+
+Select2 will respect the `disabled` property of the underlying select element. In order to enable or disable Select2, you should call `.prop('disabled', true/false)` on the ` 0 %} min="{{- min_chars -}}" {% endif %}
+            required
+            placeholder="{{"PLUGIN_SIMPLESEARCH.SEARCH_PLACEHOLDER"|t}}"
+            value="{{ query }}"
+            data-search-invalid="{{ "PLUGIN_SIMPLESEARCH.SEARCH_FIELD_MINIMUM_CHARACTERS"|t(min_chars)|raw }}"
+            data-search-separator="{{ config.system.param_sep }}"
+            data-search-input="{{ base_url }}{{ config.plugins.simplesearch.route == '@self' ? '' : (config.plugins.simplesearch.route == '/' ? '' : config.plugins.simplesearch.route) }}/query"
+        />
+        {% if config.plugins.simplesearch.display_button %}
+            
+        {% endif %}
+    
+
diff --git a/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.html.twig b/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.html.twig
new file mode 100644
index 000000000..215eeafbd
--- /dev/null
+++ b/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.html.twig
@@ -0,0 +1,24 @@
+{% extends 'partials/simplesearch_base.html.twig' %}
+
+{% block content %}
+    
+

{{"PLUGIN_SIMPLESEARCH.SEARCH_RESULTS"|t}}

+
+ {% include 'partials/simplesearch_searchbox.html.twig' %} +
+ +

+ {% if query %} + {% set count = search_results ? search_results.count : 0 %} + {% if count == 1 %} + {{ "PLUGIN_SIMPLESEARCH.SEARCH_RESULTS_SUMMARY_SINGULAR"|t(query)|raw }} + {% else %} + {{ "PLUGIN_SIMPLESEARCH.SEARCH_RESULTS_SUMMARY_PLURAL"|t(query, count)|raw }} + {% endif %} + {% endif %} +

+ {% for page in search_results %} + {% include 'partials/simplesearch_item.html.twig' with {'page':page} %} + {% endfor %} +
+{% endblock %} diff --git a/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.json.twig b/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.json.twig new file mode 100644 index 000000000..d62f50298 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.json.twig @@ -0,0 +1,5 @@ +{"results":[ +{%- for search_result in search_results -%} +{{- search_result.route|json_encode -}}{{ not loop.last ? ',' }} +{%- endfor -%} +]} diff --git a/netbox/project-static/select2-4.0.12/docs/screenshot.jpg b/netbox/project-static/select2-4.0.12/docs/screenshot.jpg new file mode 100644 index 000000000..8fa0b4e60 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/screenshot.jpg differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/.gitkeep b/netbox/project-static/select2-4.0.12/docs/themes/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/CHANGELOG.md b/netbox/project-static/select2-4.0.12/docs/themes/learn2/CHANGELOG.md new file mode 100644 index 000000000..929f7c479 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/CHANGELOG.md @@ -0,0 +1,102 @@ +# v1.7.0 +## 05/xx/2017 + +1. [](#improved) + * Added default page template. + * Added blueprints for docs and chapter pages + +# v1.6.3 +## 01/31/2017 + +1. [](#bugfix) + * Fixed changelog date + +# v1.6.2 +## 01/31/2017 + +1. [](#bugfix) + * Fixed a PHP 7.1 issue + +# v1.6.1 +## 01/24/2017 + +1. [](#new) + * Updated to FontAwesome 4.7.0 with [Grav icon](http://fontawesome.io/icon/grav/) + +# v1.6.0 +## 07/14/2016 + +1. [](#new) + * Added the spanish language +1. [](#improved) + * Remove unneeded streams from Theme YAML + * Set the page language from Grav's Language configuration. Default to english. +1. [](#bugfix) + * Fix an issue on iOS 9+ Safari scaling + +# v1.5.0 +## 01/06/2016 + +1. [](#new) + * Added keyboard prev/next navigation +1. [](#improved) + * Various language updates +1. [](#bugfix) + * Fixed a typo in CSS + +# v1.4.2 +## 12/18/2015 + +1. [](#bugfix) + * Fixed clipboard for Safari + +# v1.4.1 +## 12/11/2015 + +1. [](#new) + * Support new sidebar scrollbar + * New subtle `subtitle` styling + +# v1.4.0 +## 10/07/2015 + +1. [](#new) + * Added 1-click copy-to-clipboard feature for `code` and `pre` tags + * Added German translations + * Configurable root page +1. [](#improved) + * Wrapped topbar to remove it from error pages +1. [](#bugfix) + * Fix for bad YAML + * Fix for bad HTML in github note + +# v1.3.0 +## 09/11/2015 + +1. [](#new) + * Added configurable Google analytics code + +# v1.2.0 +## 08/25/2015 + +1. [](#improved) + * Added blueprints for Grav Admin plugin + +# v1.1.0 +## 07/19/2015 + +1. [](#new) + * Added search highlight support + * Added a footer + +# v1.0.1 +## 06/2/2015 + +1. [](#new) + * Added support for 2+ page levels + +# v1.0.0 +## 06/17/2015 + +1. [](#new) + * ChangeLog started... diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/LICENSE b/netbox/project-static/select2-4.0.12/docs/themes/learn2/LICENSE new file mode 100644 index 000000000..484793ad1 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Grav + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/README.md b/netbox/project-static/select2-4.0.12/docs/themes/learn2/README.md new file mode 100644 index 000000000..0160c67d0 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/README.md @@ -0,0 +1,79 @@ +# Learn2 + +![Learn2](screenshot.jpg) + +Learn2 is the default [Grav Learn](http://learn.getgrav.org) theme. Simple, fast and modern. + +# Installation + +Installing the Learn2 theme can be done in one of two ways. Our GPM (Grav Package Manager) installation method enables you to quickly and easily install the theme with a simple terminal command, while the manual method enables you to do so via a zip file. + +The theme is designed to be used to provide a documentation site. You can see this in action at [](http://learn.getgrav.org) + +## GPM Installation (Preferred) + +The simplest way to install this theme is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm) through your system's Terminal (also called the command line). From the root of your Grav install type: + + bin/gpm install learn2 + +This will install the Learn2 theme into your `/user/themes` directory within Grav. Its files can be found under `/your/site/grav/user/themes/learn2`. + +## Manual Installation + +To install this theme, just download the zip version of this repository and unzip it under `/your/site/grav/user/themes`. Then, rename the folder to `learn2`. You can find these files either on [GitHub](https://github.com/getgrav/grav-theme-learn2) or via [GetGrav.org](http://getgrav.org/downloads/themes). + +You should now have all the theme files under + + /your/site/grav/user/themes/learn2 + +>> NOTE: This theme is a modular component for Grav which requires the [Grav](http://github.com/getgrav/grav), [Error](https://github.com/getgrav/grav-theme-error) and [Problems](https://github.com/getgrav/grav-plugin-problems) plugins. + +# Updating + +As development for the Learn2 theme continues, new versions may become available that add additional features and functionality, improve compatibility with newer Grav releases, and generally provide a better user experience. Updating Learn2 is easy, and can be done through Grav's GPM system, as well as manually. + +## GPM Update (Preferred) + +The simplest way to update this theme is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm). You can do this with this by navigating to the root directory of your Grav install using your system's Terminal (also called command line) and typing the following: + + bin/gpm update learn2 + +This command will check your Grav install to see if your Learn2 theme is due for an update. If a newer release is found, you will be asked whether or not you wish to update. To continue, type `y` and hit enter. The theme will automatically update and clear Grav's cache. + +## Manual Update + +Manually updating Learn2 is pretty simple. Here is what you will need to do to get this done: + +* Delete the `your/site/user/themes/learn2` directory. +* Download the new version of the Learn2 theme from either [GitHub](https://github.com/getgrav/grav-theme-learn2) or [GetGrav.org](http://getgrav.org/downloads/themes#extras). +* Unzip the zip file in `your/site/user/themes` and rename the resulting folder to `learn2`. +* Clear the Grav cache. The simplest way to do this is by going to the root Grav directory in terminal and typing `bin/grav clear-cache`. + +> Note: Any changes you have made to any of the files listed under this directory will also be removed and replaced by the new set. Any files located elsewhere (for example a YAML settings file placed in `user/config/themes`) will remain intact. + +## Features + +* Lightweight and minimal for optimal performance +* Fully responsive with off-page mobile navigation +* SCSS based CSS source files for easy customization +* Built specifically for providing easy to read documentation +* Fontawesome icon support + +### Supported Page Templates + +* "Docs" template +* "Chapter" template +* Error view template + + +## Setup + +If you want to set Learn2 as the default theme, you can do so by following these steps: + +* Navigate to `/your/site/grav/user/config`. +* Open the **system.yaml** file. +* Change the `theme:` setting to `theme: learn2`. +* Save your changes. +* Clear the Grav cache. The simplest way to do this is by going to the root Grav directory in Terminal and typing `bin/grav clear-cache`. + +Once this is done, you should be able to see the new theme on the frontend. Keep in mind any customizations made to the previous theme will not be reflected as all of the theme and templating information is now being pulled from the **learn2** folder. diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints.yaml b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints.yaml new file mode 100644 index 000000000..34bff4076 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints.yaml @@ -0,0 +1,66 @@ +name: Learn2 +version: 1.6.3 +description: "Learn2 is a new modern documentation theme for Grav" +icon: book +author: + name: Team Grav + email: devs@getgrav.org + url: http://getgrav.org +homepage: https://github.com/getgrav/grav-theme-learn2 +demo: http://learn.getgrav.org +keywords: heme, docs, modern, fast, responsive, html5, css3 +bugs: https://github.com/getgrav/grav-theme-learn2/issues +license: MIT + +form: + validation: loose + fields: + top_level_version: + type: toggle + label: Top Level Version + highlight: 1 + default: 0 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + + home_url: + type: text + label: Home URL + placeholder: http://getgrav.org + validate: + type: text + + google_analytics_code: + type: text + label: Google Analytics Code + placeholder: UA-XXXXXXXX-X + validate: + type: text + + github.position: + type: select + size: medium + classes: fancy + label: GitHub Position + options: + top: Top + bottom: Bottom + off: Off + + github.tree: + type: text + label: GitHub Tree + default: https://github.com/getgrav/grav-skeleton-rtfm-site/blob/develop/ + + github.commits: + type: text + label: GitHub Tree + default: https://github.com/getgrav/grav-skeleton-rtfm-site/commits/develop/ + + github.commits: + type: text + label: GitHub Tree + default: https://github.com/getgrav/grav-skeleton-rtfm-site/commits/develop/ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/chapter.yaml b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/chapter.yaml new file mode 100644 index 000000000..baa2b9164 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/chapter.yaml @@ -0,0 +1,4 @@ +title: Chapter +'@extends': + type: default + context: blueprints://pages diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/docs.yaml b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/docs.yaml new file mode 100644 index 000000000..f1d430ab2 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/docs.yaml @@ -0,0 +1,4 @@ +title: Docs +'@extends': + type: default + context: blueprints://pages diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css new file mode 100644 index 000000000..ed2c0a38a --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css @@ -0,0 +1,617 @@ +*, *::before, *::after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +@-webkit-viewport { + width: device-width; } +@-moz-viewport { + width: device-width; } +@-ms-viewport { + width: device-width; } +@-o-viewport { + width: device-width; } +@viewport { + width: device-width; } +html { + font-size: 100%; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; } + +body { + margin: 0; } + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; } + +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; } + +audio:not([controls]) { + display: none; + height: 0; } + +[hidden], +template { + display: none; } + +a { + background: transparent; + text-decoration: none; } + +a:active, +a:hover { + outline: 0; } + +abbr[title] { + border-bottom: 1px dotted; } + +b, +strong { + font-weight: bold; } + +dfn { + font-style: italic; } + +mark { + background: #FFFF27; + color: #333; } + +sub, +sup { + font-size: 0.8rem; + line-height: 0; + position: relative; + vertical-align: baseline; } + +sup { + top: -0.5em; } + +sub { + bottom: -0.25em; } + +img { + border: 0; + max-width: 100%; } + +svg:not(:root) { + overflow: hidden; } + +figure { + margin: 1em 40px; } + +hr { + height: 0; } + +pre { + overflow: auto; } + +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; } + +button { + overflow: visible; } + +button, +select { + text-transform: none; } + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; } + +button[disabled], +html input[disabled] { + cursor: default; } + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; } + +input { + line-height: normal; } + +input[type="checkbox"], +input[type="radio"] { + padding: 0; } + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; } + +input[type="search"] { + -webkit-appearance: textfield; } + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; } + +legend { + border: 0; + padding: 0; } + +textarea { + overflow: auto; } + +optgroup { + font-weight: bold; } + +table { + border-collapse: collapse; + border-spacing: 0; + table-layout: fixed; + width: 100%; } + +tr, td, th { + vertical-align: middle; } + +th, td { + padding: 0.425rem 0; } + +th { + text-align: left; } + +.container { + width: 75em; + margin: 0 auto; + padding: 0; } + @media only all and (min-width: 60em) and (max-width: 74.938em) { + .container { + width: 60em; } } + @media only all and (min-width: 48em) and (max-width: 59.938em) { + .container { + width: 48em; } } + @media only all and (min-width: 30.063em) and (max-width: 47.938em) { + .container { + width: 30em; } } + @media only all and (max-width: 30em) { + .container { + width: 100%; } } + +.grid { + display: -webkit-box; + display: -moz-box; + display: box; + display: -webkit-flex; + display: -moz-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row; + -moz-flex-flow: row; + flex-flow: row; + list-style: none; + margin: 0; + padding: 0; } + @media only all and (max-width: 47.938em) { + .grid { + -webkit-flex-flow: row wrap; + -moz-flex-flow: row wrap; + flex-flow: row wrap; } } + +.block { + -webkit-box-flex: 1; + -moz-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -moz-flex: 1; + -ms-flex: 1; + flex: 1; + min-width: 0; + min-height: 0; } + @media only all and (max-width: 47.938em) { + .block { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 100%; + -moz-flex: 0 100%; + -ms-flex: 0 100%; + flex: 0 100%; } } + +.content { + margin: 0.625rem; + padding: 0.938rem; } + +@media only all and (max-width: 47.938em) { + body [class*="size-"] { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 100%; + -moz-flex: 0 100%; + -ms-flex: 0 100%; + flex: 0 100%; } } + +.size-1-2 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 50%; + -moz-flex: 0 50%; + -ms-flex: 0 50%; + flex: 0 50%; } + +.size-1-3 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 33.33333%; + -moz-flex: 0 33.33333%; + -ms-flex: 0 33.33333%; + flex: 0 33.33333%; } + +.size-1-4 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 25%; + -moz-flex: 0 25%; + -ms-flex: 0 25%; + flex: 0 25%; } + +.size-1-5 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 20%; + -moz-flex: 0 20%; + -ms-flex: 0 20%; + flex: 0 20%; } + +.size-1-6 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 16.66667%; + -moz-flex: 0 16.66667%; + -ms-flex: 0 16.66667%; + flex: 0 16.66667%; } + +.size-1-7 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 14.28571%; + -moz-flex: 0 14.28571%; + -ms-flex: 0 14.28571%; + flex: 0 14.28571%; } + +.size-1-8 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 12.5%; + -moz-flex: 0 12.5%; + -ms-flex: 0 12.5%; + flex: 0 12.5%; } + +.size-1-9 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 11.11111%; + -moz-flex: 0 11.11111%; + -ms-flex: 0 11.11111%; + flex: 0 11.11111%; } + +.size-1-10 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 10%; + -moz-flex: 0 10%; + -ms-flex: 0 10%; + flex: 0 10%; } + +.size-1-11 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 9.09091%; + -moz-flex: 0 9.09091%; + -ms-flex: 0 9.09091%; + flex: 0 9.09091%; } + +.size-1-12 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 8.33333%; + -moz-flex: 0 8.33333%; + -ms-flex: 0 8.33333%; + flex: 0 8.33333%; } + +@media only all and (min-width: 48em) and (max-width: 59.938em) { + .size-tablet-1-2 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 50%; + -moz-flex: 0 50%; + -ms-flex: 0 50%; + flex: 0 50%; } + + .size-tablet-1-3 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 33.33333%; + -moz-flex: 0 33.33333%; + -ms-flex: 0 33.33333%; + flex: 0 33.33333%; } + + .size-tablet-1-4 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 25%; + -moz-flex: 0 25%; + -ms-flex: 0 25%; + flex: 0 25%; } + + .size-tablet-1-5 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 20%; + -moz-flex: 0 20%; + -ms-flex: 0 20%; + flex: 0 20%; } + + .size-tablet-1-6 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 16.66667%; + -moz-flex: 0 16.66667%; + -ms-flex: 0 16.66667%; + flex: 0 16.66667%; } + + .size-tablet-1-7 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 14.28571%; + -moz-flex: 0 14.28571%; + -ms-flex: 0 14.28571%; + flex: 0 14.28571%; } + + .size-tablet-1-8 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 12.5%; + -moz-flex: 0 12.5%; + -ms-flex: 0 12.5%; + flex: 0 12.5%; } + + .size-tablet-1-9 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 11.11111%; + -moz-flex: 0 11.11111%; + -ms-flex: 0 11.11111%; + flex: 0 11.11111%; } + + .size-tablet-1-10 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 10%; + -moz-flex: 0 10%; + -ms-flex: 0 10%; + flex: 0 10%; } + + .size-tablet-1-11 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 9.09091%; + -moz-flex: 0 9.09091%; + -ms-flex: 0 9.09091%; + flex: 0 9.09091%; } + + .size-tablet-1-12 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 8.33333%; + -moz-flex: 0 8.33333%; + -ms-flex: 0 8.33333%; + flex: 0 8.33333%; } } +@media only all and (max-width: 47.938em) { + @supports not (flex-wrap: wrap) { + .grid { + display: block; + -webkit-box-lines: inherit; + -moz-box-lines: inherit; + box-lines: inherit; + -webkit-flex-wrap: inherit; + -moz-flex-wrap: inherit; + -ms-flex-wrap: inherit; + flex-wrap: inherit; } + + .block { + display: block; + -webkit-box-flex: inherit; + -moz-box-flex: inherit; + box-flex: inherit; + -webkit-flex: inherit; + -moz-flex: inherit; + -ms-flex: inherit; + flex: inherit; } } } +.first-block { + -webkit-box-ordinal-group: 0; + -webkit-order: -1; + -ms-flex-order: -1; + order: -1; } + +.last-block { + -webkit-box-ordinal-group: 2; + -webkit-order: 1; + -ms-flex-order: 1; + order: 1; } + +.fixed-blocks { + -webkit-flex-flow: row wrap; + -moz-flex-flow: row wrap; + flex-flow: row wrap; } + .fixed-blocks .block { + -webkit-box-flex: inherit; + -moz-box-flex: inherit; + box-flex: inherit; + -webkit-flex: inherit; + -moz-flex: inherit; + -ms-flex: inherit; + flex: inherit; + width: 25%; } + @media only all and (min-width: 60em) and (max-width: 74.938em) { + .fixed-blocks .block { + width: 33.33333%; } } + @media only all and (min-width: 48em) and (max-width: 59.938em) { + .fixed-blocks .block { + width: 50%; } } + @media only all and (max-width: 47.938em) { + .fixed-blocks .block { + width: 100%; } } + +body { + font-size: 1.05rem; + line-height: 1.7; } + +h1, h2, h3, h4, h5, h6 { + margin: 0.85rem 0 1.7rem 0; + text-rendering: optimizeLegibility; } + +h1 { + font-size: 3.25rem; } + +h2 { + font-size: 2.55rem; } + +h3 { + font-size: 2.15rem; } + +h4 { + font-size: 1.8rem; } + +h5 { + font-size: 1.4rem; } + +h6 { + font-size: 0.9rem; } + +p { + margin: 1.7rem 0; } + +ul, ol { + margin-top: 1.7rem; + margin-bottom: 1.7rem; } + ul ul, ul ol, ol ul, ol ol { + margin-top: 0; + margin-bottom: 0; } + +blockquote { + margin: 1.7rem 0; + padding-left: 0.85rem; } + +cite { + display: block; + font-size: 0.925rem; } + cite:before { + content: "\2014 \0020"; } + +pre { + margin: 1.7rem 0; + padding: 0.938rem; } + +code { + vertical-align: bottom; } + +small { + font-size: 0.925rem; } + +hr { + border-left: none; + border-right: none; + border-top: none; + margin: 1.7rem 0; } + +fieldset { + border: 0; + padding: 0.938rem; + margin: 0 0 1.7rem 0; } + +input, +label, +select { + display: block; } + +label { + margin-bottom: 0.425rem; } + label.required:after { + content: "*"; } + label abbr { + display: none; } + +textarea, input[type="email"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="text"], input[type="url"], input[type="color"], input[type="date"], input[type="datetime"], input[type="datetime-local"], input[type="month"], input[type="time"], input[type="week"], select[multiple=multiple] { + -webkit-transition: border-color; + -moz-transition: border-color; + transition: border-color; + border-radius: 0.1875rem; + margin-bottom: 0.85rem; + padding: 0.425rem 0.425rem; + width: 100%; } + textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + outline: none; } + +textarea { + resize: vertical; } + +input[type="checkbox"], input[type="radio"] { + display: inline; + margin-right: 0.425rem; } + +input[type="file"] { + width: 100%; } + +select { + width: auto; + max-width: 100%; + margin-bottom: 1.7rem; } + +button, +input[type="submit"] { + cursor: pointer; + user-select: none; + vertical-align: middle; + white-space: nowrap; + border: inherit; } + +/*# sourceMappingURL=nucleus.css.map */ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css.map b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css.map new file mode 100644 index 000000000..8e4a50ef8 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,sBAAuB;ECSf,kBAAoB,EDRP,UAAU;ECavB,eAAiB,EDbJ,UAAU;EC4BvB,UAAY,ED5BC,UAAU;;AAG/B,iBAAqC;EAAnB,KAAK,EAAC,YAAY;AACpC,cAAkC;EAAnB,KAAK,EAAC,YAAY;AACjC,aAAiC;EAAnB,KAAK,EAAC,YAAY;AAChC,YAAgC;EAAnB,KAAK,EAAC,YAAY;AAC/B,SAA6B;EAAnB,KAAK,EAAC,YAAY;AAE5B,IAAK;EACJ,SAAS,EAAE,IAAI;EACf,oBAAoB,EAAE,IAAI;EAC1B,wBAAwB,EAAE,IAAI;;AAG/B,IAAK;EACJ,MAAM,EAAE,CAAC;;AAGV;;;;;;;;;;;OAWQ;EACP,OAAO,EAAE,KAAK;;AAGf;;;KAGM;EACL,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,QAAQ;;AAGzB,qBAAsB;EACrB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,CAAC;;AAGV;QACS;EACR,OAAO,EAAE,IAAI;;AAGd,CAAE;EACD,UAAU,EAAE,WAAW;EACvB,eAAe,EAAE,IAAI;;AAGtB;OACQ;EACP,OAAO,EAAE,CAAC;;AAGX,WAAY;EACX,aAAa,EAAE,UAAU;;AAG1B;MACO;EACN,WAAW,EAAE,IAAI;;AAGlB,GAAI;EACH,UAAU,EAAE,MAAM;;AAGnB,IAAK;EACJ,UAAU,EAAE,OAAO;EACnB,KAAK,EAAE,IAAI;;AAGZ;GACI;EACH,SAAS,EAAE,MAAuB;EAClC,WAAW,EAAE,CAAC;EACd,QAAQ,EAAE,QAAQ;EAClB,cAAc,EAAE,QAAQ;;AAGzB,GAAI;EACH,GAAG,EAAE,MAAM;;AAGZ,GAAI;EACH,MAAM,EAAE,OAAO;;AAGhB,GAAI;EACH,MAAM,EAAE,CAAC;EACT,SAAS,EAAE,IAAI;;AAGhB,cAAe;EACd,QAAQ,EAAE,MAAM;;AAGjB,MAAO;EACN,MAAM,EAAE,QAAQ;;AAGjB,EAAG;EACF,MAAM,EAAE,CAAC;;AAGV,GAAI;EACH,QAAQ,EAAE,IAAI;;AAUf;;;;QAIS;EACR,KAAK,EAAE,OAAO;EACd,IAAI,EAAE,OAAO;EACb,MAAM,EAAE,CAAC;;AAGV,MAAO;EACN,QAAQ,EAAE,OAAO;;AAGlB;MACO;EACN,cAAc,EAAE,IAAI;;AAGrB;;;oBAGqB;EACpB,kBAAkB,EAAE,MAAM;EAC1B,MAAM,EAAE,OAAO;;AAGhB;oBACqB;EACpB,MAAM,EAAE,OAAO;;AAGhB;uBACwB;EACvB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAGX,KAAM;EACL,WAAW,EAAE,MAAM;;AAGpB;mBACoB;EACnB,OAAO,EAAE,CAAC;;AAGX;+CACgD;EAC/C,MAAM,EAAE,IAAI;;AAGb,oBAAqB;EACpB,kBAAkB,EAAE,SAAS;;AAG9B;+CACgD;EAC/C,kBAAkB,EAAE,IAAI;;AAGzB,MAAO;EACN,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAGX,QAAS;EACR,QAAQ,EAAE,IAAI;;AAGf,QAAS;EACR,WAAW,EAAE,IAAI;;AAGlB,KAAM;EACL,eAAe,EAAE,QAAQ;EACzB,cAAc,EAAE,CAAC;EACjB,YAAY,EAAE,KAAK;EACnB,KAAK,EAAE,IAAI;;AAGZ,UAAW;EACV,cAAc,EAAE,MAAM;;AAGvB,MAAO;EACN,OAAO,EAAE,UAAuB;;AAGjC,EAAG;EACF,UAAU,EAAE,IAAI;;AEtNjB,UAAW;EACV,KAAK,ECDqB,IAAQ;EDElC,MAAM,EAAE,MAAM;EACd,OAAO,EAAE,CAAC;EEET,+DAA4G;IFL9G,UAAW;MAKT,KAAK,ECJgB,IAAQ;ECO7B,+DAAqG;IFRvG,UAAW;MAQT,KAAK,ECNe,IAAQ;ECS5B,mEAAkH;IFXpH,UAAW;MAWT,KAAK,ECRmB,IAAQ;ECWhC,qCAA+D;IFdjE,UAAW;MAcT,KAAK,ECVe,IAAI;;ADe1B,KAAM;EGiDE,OAAO,EAAE,WAAW;EACpB,OAAO,EAAE,QAAQ;EACjB,OAAO,EAAE,GAAG;EAGZ,OAAO,EAAE,YAAY;EACrB,OAAO,EAAE,SAAS;EAClB,OAAO,EAAE,WAAW;EACpB,OAAO,EAAE,IAAI;EJpEb,iBAAoB,ECaR,GAAG;EDRf,cAAiB,ECQL,GAAG;EDOf,SAAY,ECPA,GAAG;EACtB,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EEJT,yCAAiE;IFDnE,KAAM;MDXE,iBAAoB,ECkBP,QAAQ;MDbrB,cAAiB,ECaJ,QAAQ;MDErB,SAAY,ECFC,QAAQ;;AAI7B,MAAO;EDtBC,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECuBb,CAAC;EDlBR,SAAiB,ECkBV,CAAC;EDbR,QAAgB,ECaT,CAAC;EDHR,IAAY,ECGL,CAAC;EACZ,SAAS,EAAE,CAAC;EACZ,UAAU,EAAE,CAAC;EEbf,yCAAiE;IFUnE,MAAO;MDtBC,gBAAoB,EI6FZ,CAAc;MJxFtB,aAAiB,EIwFT,CAAc;MJzEtB,QAAY,EIyEJ,CAAc;MJ7FtB,YAAoB,EC2BZ,MAAM;MDtBd,SAAiB,ECsBT,MAAM;MDjBd,QAAgB,ECiBR,MAAM;MDPd,IAAY,ECOJ,MAAM;;AAKtB,QAAS;EACR,MAAM,EIzCa,QAAQ;EJ0C3B,OAAO,EIzCa,QAAQ;;AFmB3B,yCAAiE;EFyBnE,qBAAsB;IDrCd,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECuCZ,MAAM;IDlCd,SAAiB,ECkCT,MAAM;ID7Bd,QAAgB,EC6BR,MAAM;IDnBd,IAAY,ECmBJ,MAAM;;AAKtB,SAAU;ED5CF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,EC6Cb,KAAiB;EDxCxB,SAAiB,ECwCV,KAAiB;EDnCxB,QAAgB,ECmCT,KAAiB;EDzBxB,IAAY,ECyBL,KAAiB;;AAGhC,SAAU;EDhDF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECiDb,WAAiB;ED5CxB,SAAiB,EC4CV,WAAiB;EDvCxB,QAAgB,ECuCT,WAAiB;ED7BxB,IAAY,EC6BL,WAAiB;;AAGhC,SAAU;EDpDF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECqDb,KAAiB;EDhDxB,SAAiB,ECgDV,KAAiB;ED3CxB,QAAgB,EC2CT,KAAiB;EDjCxB,IAAY,ECiCL,KAAiB;;AAGhC,SAAU;EDxDF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECyDb,KAAiB;EDpDxB,SAAiB,ECoDV,KAAiB;ED/CxB,QAAgB,EC+CT,KAAiB;EDrCxB,IAAY,ECqCL,KAAiB;;AAGhC,SAAU;ED5DF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,EC6Db,WAAiB;EDxDxB,SAAiB,ECwDV,WAAiB;EDnDxB,QAAgB,ECmDT,WAAiB;EDzCxB,IAAY,ECyCL,WAAiB;;AAGhC,SAAU;EDhEF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECiEb,WAAiB;ED5DxB,SAAiB,EC4DV,WAAiB;EDvDxB,QAAgB,ECuDT,WAAiB;ED7CxB,IAAY,EC6CL,WAAiB;;AAGhC,SAAU;EDpEF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECqEb,OAAiB;EDhExB,SAAiB,ECgEV,OAAiB;ED3DxB,QAAgB,EC2DT,OAAiB;EDjDxB,IAAY,ECiDL,OAAiB;;AAGhC,SAAU;EDxEF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECyEb,WAAiB;EDpExB,SAAiB,ECoEV,WAAiB;ED/DxB,QAAgB,EC+DT,WAAiB;EDrDxB,IAAY,ECqDL,WAAiB;;AAGhC,UAAW;ED5EH,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,EC6Eb,KAAkB;EDxEzB,SAAiB,ECwEV,KAAkB;EDnEzB,QAAgB,ECmET,KAAkB;EDzDzB,IAAY,ECyDL,KAAkB;;AAGjC,UAAW;EDhFH,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECiFb,UAAkB;ED5EzB,SAAiB,EC4EV,UAAkB;EDvEzB,QAAgB,ECuET,UAAkB;ED7DzB,IAAY,EC6DL,UAAkB;;AAGjC,UAAW;EDpFH,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECqFb,UAAkB;EDhFzB,SAAiB,ECgFV,UAAkB;ED3EzB,QAAgB,EC2ET,UAAkB;EDjEzB,IAAY,ECiEL,UAAkB;;AErF/B,+DAAqG;EFyFtG,gBAAiB;IDzFV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC0FZ,KAAiB;IDrFzB,SAAiB,ECqFT,KAAiB;IDhFzB,QAAgB,ECgFR,KAAiB;IDtEzB,IAAY,ECsEJ,KAAiB;;EAGhC,gBAAiB;ID7FV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC8FZ,WAAiB;IDzFzB,SAAiB,ECyFT,WAAiB;IDpFzB,QAAgB,ECoFR,WAAiB;ID1EzB,IAAY,EC0EJ,WAAiB;;EAGhC,gBAAiB;IDjGV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECkGZ,KAAiB;ID7FzB,SAAiB,EC6FT,KAAiB;IDxFzB,QAAgB,ECwFR,KAAiB;ID9EzB,IAAY,EC8EJ,KAAiB;;EAGhC,gBAAiB;IDrGV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECsGZ,KAAiB;IDjGzB,SAAiB,ECiGT,KAAiB;ID5FzB,QAAgB,EC4FR,KAAiB;IDlFzB,IAAY,ECkFJ,KAAiB;;EAGhC,gBAAiB;IDzGV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC0GZ,WAAiB;IDrGzB,SAAiB,ECqGT,WAAiB;IDhGzB,QAAgB,ECgGR,WAAiB;IDtFzB,IAAY,ECsFJ,WAAiB;;EAGhC,gBAAiB;ID7GV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC8GZ,WAAiB;IDzGzB,SAAiB,ECyGT,WAAiB;IDpGzB,QAAgB,ECoGR,WAAiB;ID1FzB,IAAY,EC0FJ,WAAiB;;EAGhC,gBAAiB;IDjHV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECkHZ,OAAiB;ID7GzB,SAAiB,EC6GT,OAAiB;IDxGzB,QAAgB,ECwGR,OAAiB;ID9FzB,IAAY,EC8FJ,OAAiB;;EAGhC,gBAAiB;IDrHV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECsHZ,WAAiB;IDjHzB,SAAiB,ECiHT,WAAiB;ID5GzB,QAAgB,EC4GR,WAAiB;IDlGzB,IAAY,ECkGJ,WAAiB;;EAGhC,iBAAkB;IDzHX,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC0HZ,KAAkB;IDrH1B,SAAiB,ECqHT,KAAkB;IDhH1B,QAAgB,ECgHR,KAAkB;IDtG1B,IAAY,ECsGJ,KAAkB;;EAGjC,iBAAkB;ID7HX,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC8HZ,UAAkB;IDzH1B,SAAiB,ECyHT,UAAkB;IDpH1B,QAAgB,ECoHR,UAAkB;ID1G1B,IAAY,EC0GJ,UAAkB;;EAGjC,iBAAkB;IDjIX,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECkIZ,UAAkB;ID7H1B,SAAiB,EC6HT,UAAkB;IDxH1B,QAAgB,ECwHR,UAAkB;ID9G1B,IAAY,EC8GJ,UAAkB;AEtHhC,yCAAiE;EF4HlE,+BASC;IARA,KAAM;MACL,OAAO,EAAE,KAAK;MD1IT,iBAAoB,EIsJZ,OAAM;MJjJd,cAAiB,EIiJT,OAAM;MJlId,SAAY,EIkIJ,OAAM;MJtJd,iBAAoB,EIsJZ,OAAM;MJjJd,cAAiB,EIiJT,OAAM;MJ5Id,aAAgB,EI4IR,OAAM;MJlId,SAAY,EIkIJ,OAAM;;IHTpB,MAAO;MACN,OAAO,EAAE,KAAK;MD9IT,gBAAoB,EI6FZ,OAAc;MJxFtB,aAAiB,EIwFT,OAAc;MJzEtB,QAAY,EIyEJ,OAAc;MJ7FtB,YAAoB,EI6FZ,OAAc;MJxFtB,SAAiB,EIwFT,OAAc;MJnFtB,QAAgB,EImFR,OAAc;MJzEtB,IAAY,EIyEJ,OAAc;AHwD9B,YAAa;EACX,yBAAyB,EAAE,CAAC;EAC5B,aAAa,EAAE,EAAE;EACjB,cAAc,EAAE,EAAE;EAClB,KAAK,EAAE,EAAE;;AAGX,WAAY;EACV,yBAAyB,EAAE,CAAC;EAC5B,aAAa,EAAE,CAAC;EAChB,cAAc,EAAE,CAAC;EACjB,KAAK,EAAE,CAAC;;AAIV,aAAc;EDpKN,iBAAoB,ECqKR,QAAQ;EDhKpB,cAAiB,ECgKL,QAAQ;EDjJpB,SAAY,ECiJA,QAAQ;EAC3B,oBAAO;IDtKA,gBAAoB,EI6FZ,OAAc;IJxFtB,aAAiB,EIwFT,OAAc;IJzEtB,QAAY,EIyEJ,OAAc;IJ7FtB,YAAoB,EI6FZ,OAAc;IJxFtB,SAAiB,EIwFT,OAAc;IJnFtB,QAAgB,EImFR,OAAc;IJzEtB,IAAY,EIyEJ,OAAc;IH2E5B,KAAK,EI5Ke,GAAe;IFCnC,+DAA4G;MFyK7G,oBAAO;QAIL,KAAK,EI7KgB,SAAe;IFGrC,+DAAqG;MFsKtG,oBAAO;QAOL,KAAK,EI/Ke,GAAe;IFcpC,yCAAiE;MF0JlE,oBAAO;QAUL,KAAK,EAAE,IAAI;;AKxLd,IAAK;EACJ,SAAS,ECDU,OAAO;EDE1B,WAAW,ECDU,GAAG;;ADKzB,sBAAuB;EACtB,MAAM,EAAE,kBAAuC;EAC/C,cAAc,EAAE,kBAAkB;;AAGnC,EAAG;EACF,SAAS,ECRsB,OAAuB;;ADWvD,EAAG;EACF,SAAS,ECXsB,OAAuB;;ADcvD,EAAG;EACF,SAAS,ECdsB,OAAuB;;ADiBvD,EAAG;EACF,SAAS,ECjBsB,MAAuB;;ADoBvD,EAAG;EACF,SAAS,ECpBsB,MAAuB;;ADuBvD,EAAG;EACF,SAAS,ECvBsB,MAAuB;;AD2BvD,CAAE;EACD,MAAM,EAAE,QAAiB;;AAI1B,MAAO;EACN,UAAU,EC9BS,MAAwB;ED+B3C,aAAa,EC/BM,MAAwB;EDgC3C,0BAAO;IACN,UAAU,EAAE,CAAC;IACb,aAAa,EAAE,CAAC;;AAKlB,UAAW;EACV,MAAM,EAAE,QAAiB;EACzB,YAAY,EAAE,OAAmB;;AAGlC,IAAK;EACJ,OAAO,EAAE,KAAK;EACd,SAAS,EAAE,QAAuB;EAClC,WAAS;IACJ,OAAO,EAAE,aAAa;;AAK5B,GAAI;EACH,MAAM,EAAE,QAAiB;EACxB,OAAO,EDlEY,QAAQ;;ACqE7B,IAAK;EACJ,cAAc,EAAE,MAAM;;AAIvB,KAAM;EACL,SAAS,EAAE,QAAuB;;AAGnC,EAAG;EACF,WAAW,EAAE,IAAI;EACjB,YAAY,EAAE,IAAI;EAClB,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,QAAiB;;AEpF1B,QAAS;EACR,MAAM,EAAE,CAAC;EACT,OAAO,EHAa,QAAQ;EGC5B,MAAM,EAAE,YAAqB;;AAG9B;;MAEO;EACN,OAAO,EAAE,KAAK;;AAGf,KAAM;EACL,aAAa,EAAE,QAAmB;EAElC,oBAAiB;IAChB,OAAO,EAAE,GAAG;EAGb,UAAK;IACJ,OAAO,EAAE,IAAI;;AAIf,kVAAyD;ERfjD,kBAAoB,EAAE,YAAM;EAK5B,eAAiB,EAAE,YAAM;EAezB,UAAY,EAAE,YAAM;EQH3B,aAAa,ECzBS,SAAM;ED0B5B,aAAa,EAAE,OAAmB;EAClC,OAAO,EAAE,iBAA2C;EACpD,KAAK,EAAE,IAAI;EAEX,kbAAQ;IACP,OAAO,EAAE,IAAI;;AAIf,QAAS;EACR,MAAM,EAAE,QAAQ;;AAGjB,2CAA4C;EAC3C,OAAO,EAAE,MAAM;EACf,YAAY,EAAE,QAAmB;;AAGlC,kBAAmB;EAClB,KAAK,EAAE,IAAI;;AAGZ,MAAO;EACN,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,IAAI;EACf,aAAa,EDvCM,MAAwB;;AC0C5C;oBACqB;EACpB,MAAM,EAAE,OAAO;EACf,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,MAAM;EACtB,WAAW,EAAE,MAAM;EACnB,MAAM,EAAE,OAAO", +"sources": ["../scss/nucleus/_core.scss","../scss/vendor/bourbon/addons/_prefixer.scss","../scss/nucleus/_flex.scss","../scss/configuration/nucleus/_breakpoints.scss","../scss/nucleus/mixins/_breakpoints.scss","../scss/vendor/bourbon/css3/_flex-box.scss","../scss/configuration/nucleus/_layout.scss","../scss/nucleus/_typography.scss","../scss/configuration/nucleus/_typography.scss","../scss/nucleus/_forms.scss","../scss/configuration/nucleus/_core.scss"], +"names": [], +"file": "nucleus.css" +} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css new file mode 100644 index 000000000..2965e5fd4 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css @@ -0,0 +1,940 @@ +@charset "UTF-8"; +@import url(//fonts.googleapis.com/css?family=Montserrat:400|Muli:300,400|Inconsolata); +#top-github-link, #body #breadcrumbs { + position: relative; + top: 50%; + -webkit-transform: translateY(-50%); + -moz-transform: translateY(-50%); + -o-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); } + +.button, .button-secondary { + display: inline-block; + padding: 7px 12px; } + .button:active, .button-secondary:active { + margin: 2px 0 -2px 0; } + +body { + background: #fff; + color: #555; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + +a { + color: #1694CA; } + a:hover { + color: #0e6185; } + +pre { + position: relative; } + +.bg { + background: #fff; + border: 1px solid #eaeaea; } + +b, strong, label, th { + font-weight: 600; } + +.default-animation, #header #logo-svg, #header #logo-svg path, #sidebar, #sidebar ul, #body, #body .padding, #body .nav { + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + transition: all 0.5s ease; } + +fieldset { + border: 1px solid #ddd; } + +textarea, input[type="email"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="text"], input[type="url"], input[type="color"], input[type="date"], input[type="datetime"], input[type="datetime-local"], input[type="month"], input[type="time"], input[type="week"], select[multiple=multiple] { + background-color: white; + border: 1px solid #ddd; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.06); } + textarea:hover, input[type="email"]:hover, input[type="number"]:hover, input[type="password"]:hover, input[type="search"]:hover, input[type="tel"]:hover, input[type="text"]:hover, input[type="url"]:hover, input[type="color"]:hover, input[type="date"]:hover, input[type="datetime"]:hover, input[type="datetime-local"]:hover, input[type="month"]:hover, input[type="time"]:hover, input[type="week"]:hover, select[multiple=multiple]:hover { + border-color: #c4c4c4; } + textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + border-color: #1694CA; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.06), 0 0 5px rgba(19, 131, 179, 0.7); } + +#header { + background: #1694CA; + color: #fff; + text-align: center; + padding: 1rem; } + #header a { + display: inline-block; } + #header #logo-svg { + width: 8rem; + height: 2rem; } + #header #logo-svg path { + fill: #fff; } + +.searchbox { + margin-top: 0.5rem; + position: relative; + border: 1px solid #19a5e1; + background: #1383b3; + border-radius: 4px; } + .searchbox label { + color: rgba(255, 255, 255, 0.8); + position: absolute; + left: 10px; + top: 3px; } + .searchbox span { + color: rgba(255, 255, 255, 0.6); + position: absolute; + right: 10px; + top: 3px; + cursor: pointer; } + .searchbox span:hover { + color: rgba(255, 255, 255, 0.9); } + .searchbox input { + display: inline-block; + color: #fff; + width: 100%; + height: 30px; + background: transparent; + border: 0; + padding: 0 25px 0 30px; + margin: 0; + font-weight: 400; } + .searchbox input::-webkit-input-placeholder { + color: rgba(255, 255, 255, 0.6); } + .searchbox input::-moz-placeholder { + color: rgba(255, 255, 255, 0.6); } + .searchbox input:-moz-placeholder { + color: rgba(255, 255, 255, 0.6); } + .searchbox input:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.6); } + +#sidebar-toggle { + display: none; } + @media only all and (max-width: 47.938em) { + #sidebar-toggle { + display: inline-block; } } + +#sidebar { + background-color: #38424D; + position: fixed; + top: 0; + width: 300px; + bottom: 0; + left: 0; + font-weight: 500; + font-size: 15px; } + #sidebar a { + color: #bbbbbb; } + #sidebar a:hover { + color: #d5d5d5; } + #sidebar a.subtitle { + color: rgba(187, 187, 187, 0.6); } + #sidebar hr { + border-bottom: 1px solid #323a44; } + #sidebar a.padding { + padding: 0 1rem; } + #sidebar h5 { + margin: 2rem 0 0; + position: relative; + line-height: 2; } + #sidebar h5 a { + display: block; + margin-left: 0; + margin-right: 0; + padding-left: 1rem; + padding-right: 1rem; } + #sidebar h5 i { + color: rgba(187, 187, 187, 0.6); + position: absolute; + right: 0.6rem; + top: 0.7rem; + font-size: 80%; } + #sidebar h5.parent a { + background: #293038; + color: #c8c8c8 !important; } + #sidebar h5.active a { + background: #fff; + color: #555 !important; } + #sidebar h5.active i { + color: #555 !important; } + #sidebar h5 + ul.topics { + display: none; + margin-top: 0; } + #sidebar h5.parent + ul.topics, #sidebar h5.active + ul.topics { + display: block; } + #sidebar ul { + list-style: none; + padding: 0; + margin: 0; } + #sidebar ul.searched a { + color: #888888; } + #sidebar ul.searched .search-match a { + color: #d5d5d5; } + #sidebar ul.searched .search-match a:hover { + color: #eeeeee; } + #sidebar ul.topics { + margin: 0 1rem; } + #sidebar ul.topics.searched ul { + display: block; } + #sidebar ul.topics ul { + display: none; + padding-bottom: 1rem; } + #sidebar ul.topics ul ul { + padding-bottom: 0; } + #sidebar ul.topics li.parent ul, #sidebar ul.topics > li.active ul { + display: block; } + #sidebar ul.topics > li > a { + line-height: 2rem; + font-size: 1.1rem; } + #sidebar ul.topics > li > a b { + opacity: 0.5; + font-weight: normal; } + #sidebar ul.topics > li > a .fa { + margin-top: 9px; } + #sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { + background: #2d353e; + margin-left: -1rem; + margin-right: -1rem; + padding-left: 1rem; + padding-right: 1rem; } + #sidebar ul li.active > a { + background: #fff; + color: #555 !important; + margin-left: -1rem; + margin-right: -1rem; + padding-left: 1rem; + padding-right: 1rem; } + #sidebar ul li { + padding: 0; } + #sidebar ul li.visited + span { + margin-right: 16px; } + #sidebar ul li a { + display: block; + padding: 2px 0; } + #sidebar ul li a span { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: block; } + #sidebar ul li > a { + padding: 4px 0; } + #sidebar ul li .fa { + display: none; + float: right; + font-size: 13px; + min-width: 16px; + margin: 4px 0 0 0; + text-align: right; } + #sidebar ul li.visited > a .read-icon { + color: #1694CA; + display: inline; } + #sidebar ul li li { + padding-left: 1rem; + text-indent: 0.2rem; } + +#main { + background: #f7f7f7; + margin: 0 0 1.563rem 0; } + +#body { + position: relative; + margin-left: 300px; + min-height: 100%; } + #body img, #body .video-container { + margin: 3rem auto; + display: block; + text-align: center; } + #body img.border, #body .video-container.border { + border: 2px solid #e6e6e6 !important; + padding: 2px; } + #body img.shadow, #body .video-container.shadow { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); } + #body .bordered { + border: 1px solid #ccc; } + #body .padding { + padding: 3rem 6rem; } + @media only all and (max-width: 59.938em) { + #body .padding { + position: static; + padding: 15px 3rem; } } + @media only all and (max-width: 47.938em) { + #body .padding { + padding: 5px 1rem; } } + #body h1 + hr { + margin-top: -1.7rem; + margin-bottom: 3rem; } + @media only all and (max-width: 59.938em) { + #body #navigation { + position: static; + margin-right: 0 !important; + width: 100%; + display: table; } } + #body .nav { + position: fixed; + top: 0; + bottom: 0; + width: 4rem; + font-size: 50px; + height: 100%; + cursor: pointer; + display: table; + text-align: center; } + #body .nav > i { + display: table-cell; + vertical-align: middle; + text-align: center; } + @media only all and (max-width: 59.938em) { + #body .nav { + display: table-cell; + position: static; + top: auto; + width: 50%; + text-align: center; + height: 100px; + line-height: 100px; + padding-top: 0; } + #body .nav > i { + display: inline-block; } } + #body .nav:hover { + background: #F6F6F6; } + #body .nav.nav-pref { + left: 0; } + #body .nav.nav-next { + right: 0; } + +#body-inner { + margin-bottom: 5rem; } + +#chapter { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 2rem 0; } + #chapter #body-inner { + padding-bottom: 3rem; + max-width: 80%; } + #chapter h3 { + font-family: "Muli", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + font-weight: 400; + text-align: center; } + #chapter h1 { + font-size: 5rem; + border-bottom: 4px solid #F0F2F4; } + #chapter p { + text-align: center; + font-size: 1.2rem; } + +#footer { + padding: 3rem 1rem; + color: #a2a2a2; + font-size: 13px; } + #footer p { + margin: 0; } + +body { + font-family: "Muli", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + letter-spacing: -0.03rem; + font-weight: 400; } + +h1, h2, h3, h4, h5, h6 { + font-family: "Montserrat", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + font-weight: 400; + text-rendering: optimizeLegibility; + line-height: 150%; + letter-spacing: -0px; } + +h1 { + text-align: center; + letter-spacing: -3px; } + +h2 { + letter-spacing: -2px; } + +h3 { + letter-spacing: -1px; } + +blockquote { + border-left: 10px solid #F0F2F4; } + blockquote p { + font-size: 1.1rem; + color: #999; } + blockquote cite { + display: block; + text-align: right; + color: #666; + font-size: 1.2rem; } + +blockquote { + position: relative; } + +blockquote blockquote { + position: static; } + +blockquote > blockquote > blockquote { + margin: 0; } + blockquote > blockquote > blockquote p { + padding: 15px; + display: block; + font-size: 1rem; + margin-top: 0rem; + margin-bottom: 0rem; + color: #666; } + blockquote > blockquote > blockquote p:first-child:before { + position: absolute; + top: 2px; + color: #fff; + font-family: FontAwesome; + content: ''; + left: 10px; } + blockquote > blockquote > blockquote p:first-child:after { + position: absolute; + top: 2px; + color: #fff; + left: 2rem; + font-weight: bold; + content: 'Info'; } + blockquote > blockquote > blockquote > p { + margin-left: -71px; + border-top: 30px solid #F0B37E; + background: #FFF2DB; } + blockquote > blockquote > blockquote > blockquote > p { + margin-left: -94px; + border-top: 30px solid rgba(217, 83, 79, 0.8); + background: #FAE2E2; } + blockquote > blockquote > blockquote > blockquote > p:first-child:after { + content: 'Warning'; } + blockquote > blockquote > blockquote > blockquote > blockquote > p { + margin-left: -118px; + border-top: 30px solid #6AB0DE; + background: #E7F2FA; } + blockquote > blockquote > blockquote > blockquote > blockquote > p:first-child:after { + content: 'Note'; } + blockquote > blockquote > blockquote > blockquote > blockquote > blockquote > p { + margin-left: -142px; + border-top: 30px solid rgba(92, 184, 92, 0.8); + background: #E6F9E6; } + blockquote > blockquote > blockquote > blockquote > blockquote > blockquote > p:first-child:after { + content: 'Tip'; } + +code, +kbd, +pre, +samp { + font-family: "Inconsolata", monospace; } + +code { + background: #f9f2f4; + color: #9c1d3d; + padding: .2rem .4rem; + border-radius: 3px; } + +pre { + padding: 1rem; + margin: 2rem 0; + background: #f6f6f6; + border: 1px solid #ddd; + border-radius: 2px; + line-height: 1.15; + font-size: 1rem; } + pre code { + color: #237794; + background: inherit; + font-size: 1rem; } + +hr { + border-bottom: 4px solid #F0F2F4; } + +.page-title { + margin-top: -25px; + padding: 25px; + float: left; + clear: both; + background: #1694CA; + color: #fff; } + +#body a.anchor-link { + color: #ccc; } +#body a.anchor-link:hover { + color: #1694CA; } + +.scrollbar-inner > .scroll-element .scroll-element_track { + background-color: rgba(255, 255, 255, 0.3); } + +.scrollbar-inner > .scroll-element .scroll-bar { + background-color: #b5d1eb; } + +.scrollbar-inner > .scroll-element:hover .scroll-bar { + background-color: #ccc; } + +.scrollbar-inner > .scroll-element.scroll-draggable .scroll-bar { + background-color: #ccc; } + +table { + border: 1px solid #eaeaea; + table-layout: auto; } + +th { + background: #f7f7f7; + padding: 0.5rem; } + +td { + padding: 0.5rem; + border: 1px solid #eaeaea; } + +.button { + background: #1694CA; + color: #fff; + box-shadow: 0 3px 0 #1380ae; } + .button:hover { + background: #1380ae; + box-shadow: 0 3px 0 #106c93; + color: #fff; } + .button:active { + box-shadow: 0 1px 0 #106c93; } + +.button-secondary { + background: #F8B450; + color: #fff; + box-shadow: 0 3px 0 #f7a733; } + .button-secondary:hover { + background: #f7a733; + box-shadow: 0 3px 0 #f69b15; + color: #fff; } + .button-secondary:active { + box-shadow: 0 1px 0 #f69b15; } + +.bullets { + margin: 1.7rem 0; + margin-left: -0.85rem; + margin-right: -0.85rem; + overflow: auto; } + +.bullet { + float: left; + padding: 0 0.85rem; } + +.two-column-bullet { + width: 50%; } + @media only all and (max-width: 47.938em) { + .two-column-bullet { + width: 100%; } } + +.three-column-bullet { + width: 33.33333%; } + @media only all and (max-width: 47.938em) { + .three-column-bullet { + width: 100%; } } + +.four-column-bullet { + width: 25%; } + @media only all and (max-width: 47.938em) { + .four-column-bullet { + width: 100%; } } + +.bullet-icon { + float: left; + background: #1694CA; + padding: 0.875rem; + width: 3.5rem; + height: 3.5rem; + border-radius: 50%; + color: #fff; + font-size: 1.75rem; + text-align: center; } + +.bullet-icon-1 { + background: #1694CA; } + +.bullet-icon-2 { + background: #16cac4; } + +.bullet-icon-3 { + background: #b2ca16; } + +.bullet-content { + margin-left: 4.55rem; } + +.tooltipped { + position: relative; } + +.tooltipped:after { + position: absolute; + z-index: 1000000; + display: none; + padding: 5px 8px; + font: normal normal 11px/1.5 "Muli", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + color: #fff; + text-align: center; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-wrap: break-word; + white-space: pre; + pointer-events: none; + content: attr(aria-label); + background: rgba(0, 0, 0, 0.8); + border-radius: 3px; + -webkit-font-smoothing: subpixel-antialiased; } + +.tooltipped:before { + position: absolute; + z-index: 1000001; + display: none; + width: 0; + height: 0; + color: rgba(0, 0, 0, 0.8); + pointer-events: none; + content: ""; + border: 5px solid transparent; } + +.tooltipped:hover:before, .tooltipped:hover:after, +.tooltipped:active:before, +.tooltipped:active:after, +.tooltipped:focus:before, +.tooltipped:focus:after { + display: inline-block; + text-decoration: none; } + +.tooltipped-s:after, +.tooltipped-se:after, +.tooltipped-sw:after { + top: 100%; + right: 50%; + margin-top: 5px; } +.tooltipped-s:before, +.tooltipped-se:before, +.tooltipped-sw:before { + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; + border-bottom-color: rgba(0, 0, 0, 0.8); } + +.tooltipped-se:after { + right: auto; + left: 50%; + margin-left: -15px; } + +.tooltipped-sw:after { + margin-right: -15px; } + +.tooltipped-n:after, +.tooltipped-ne:after, +.tooltipped-nw:after { + right: 50%; + bottom: 100%; + margin-bottom: 5px; } +.tooltipped-n:before, +.tooltipped-ne:before, +.tooltipped-nw:before { + top: -5px; + right: 50%; + bottom: auto; + margin-right: -5px; + border-top-color: rgba(0, 0, 0, 0.8); } + +.tooltipped-ne:after { + right: auto; + left: 50%; + margin-left: -15px; } + +.tooltipped-nw:after { + margin-right: -15px; } + +.tooltipped-s:after, +.tooltipped-n:after { + transform: translateX(50%); } + +.tooltipped-w:after { + right: 100%; + bottom: 50%; + margin-right: 5px; + transform: translateY(50%); } +.tooltipped-w:before { + top: 50%; + bottom: 50%; + left: -5px; + margin-top: -5px; + border-left-color: rgba(0, 0, 0, 0.8); } + +.tooltipped-e:after { + bottom: 50%; + left: 100%; + margin-left: 5px; + transform: translateY(50%); } +.tooltipped-e:before { + top: 50%; + right: -5px; + bottom: 50%; + margin-top: -5px; + border-right-color: rgba(0, 0, 0, 0.8); } + +/*************** SCROLLBAR BASE CSS ***************/ +.highlightable { + padding: 25px 0 15px; } + +.scroll-wrapper { + overflow: hidden !important; + padding: 0 !important; + position: relative; } + +.scroll-wrapper > .scroll-content { + border: none !important; + box-sizing: content-box !important; + height: auto; + left: 0; + margin: 0; + max-height: none; + max-width: none !important; + overflow: scroll !important; + padding: 0; + position: relative !important; + top: 0; + width: auto !important; } + +.scroll-wrapper > .scroll-content::-webkit-scrollbar { + height: 0; + width: 0; } + +.scroll-element { + display: none; } + +.scroll-element, .scroll-element div { + box-sizing: content-box; } + +.scroll-element.scroll-x.scroll-scrollx_visible, +.scroll-element.scroll-y.scroll-scrolly_visible { + display: block; } + +.scroll-element .scroll-bar, +.scroll-element .scroll-arrow { + cursor: default; } + +.scroll-textarea > .scroll-content { + overflow: hidden !important; } + +.scroll-textarea > .scroll-content > textarea { + border: none !important; + box-sizing: border-box; + height: 100% !important; + margin: 0; + max-height: none !important; + max-width: none !important; + overflow: scroll !important; + outline: none; + padding: 2px; + position: relative !important; + top: 0; + width: 100% !important; } + +.scroll-textarea > .scroll-content > textarea::-webkit-scrollbar { + height: 0; + width: 0; } + +/*************** SIMPLE INNER SCROLLBAR ***************/ +.scrollbar-inner > .scroll-element, +.scrollbar-inner > .scroll-element div { + border: none; + margin: 0; + padding: 0; + position: absolute; + z-index: 10; } + +.scrollbar-inner > .scroll-element div { + display: block; + height: 100%; + left: 0; + top: 0; + width: 100%; } + +.scrollbar-inner > .scroll-element.scroll-x { + bottom: 2px; + height: 8px; + left: 0; + width: 100%; } + +.scrollbar-inner > .scroll-element.scroll-y { + height: 100%; + right: 2px; + top: 0; + width: 8px; } + +.scrollbar-inner > .scroll-element .scroll-element_outer { + overflow: hidden; } + +.scrollbar-inner > .scroll-element .scroll-element_outer, +.scrollbar-inner > .scroll-element .scroll-element_track, +.scrollbar-inner > .scroll-element .scroll-bar { + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; } + +.scrollbar-inner > .scroll-element .scroll-element_track, +.scrollbar-inner > .scroll-element .scroll-bar { + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; + filter: alpha(opacity=30); + opacity: 0.3; } + +/* update scrollbar offset if both scrolls are visible */ +.scrollbar-inner > .scroll-element.scroll-x.scroll-scrolly_visible .scroll-element_track { + left: -12px; } + +.scrollbar-inner > .scroll-element.scroll-y.scroll-scrollx_visible .scroll-element_track { + top: -12px; } + +.scrollbar-inner > .scroll-element.scroll-x.scroll-scrolly_visible .scroll-element_size { + left: -12px; } + +.scrollbar-inner > .scroll-element.scroll-y.scroll-scrollx_visible .scroll-element_size { + top: -12px; } + +.lightbox-active #body { + overflow: visible; } + .lightbox-active #body .padding { + overflow: visible; } + +#github-contrib i { + vertical-align: middle; } + +.featherlight img { + margin: 0 !important; } + +.lifecycle #body-inner ul { + list-style: none; + margin: 0; + padding: 2rem 0 0; + position: relative; } +.lifecycle #body-inner ol { + margin: 1rem 0 1rem 0; + padding: 2rem; + position: relative; } + .lifecycle #body-inner ol li { + margin-left: 1rem; } + .lifecycle #body-inner ol strong, .lifecycle #body-inner ol label, .lifecycle #body-inner ol th { + text-decoration: underline; } + .lifecycle #body-inner ol ol { + margin-left: -1rem; } +.lifecycle #body-inner h3[class*='level'] { + font-size: 20px; + position: absolute; + margin: 0; + padding: 4px 10px; + right: 0; + z-index: 1000; + color: #fff; + background: #1ABC9C; } +.lifecycle #body-inner ol h3 { + margin-top: 1rem !important; + right: 2rem !important; } +.lifecycle #body-inner .level-1 + ol { + background: #f6fefc; + border: 4px solid #1ABC9C; + color: #16A085; } + .lifecycle #body-inner .level-1 + ol h3 { + background: #2ECC71; } +.lifecycle #body-inner .level-2 + ol { + background: #f7fdf9; + border: 4px solid #2ECC71; + color: #27AE60; } + .lifecycle #body-inner .level-2 + ol h3 { + background: #3498DB; } +.lifecycle #body-inner .level-3 + ol { + background: #f3f9fd; + border: 4px solid #3498DB; + color: #2980B9; } + .lifecycle #body-inner .level-3 + ol h3 { + background: #34495E; } +.lifecycle #body-inner .level-4 + ol { + background: #e4eaf0; + border: 4px solid #34495E; + color: #2C3E50; } + .lifecycle #body-inner .level-4 + ol h3 { + background: #34495E; } + +#top-bar { + background: #F6F6F6; + border-radius: 2px; + margin: 0rem -1rem 2rem; + padding: 0 1rem; + height: 0; + min-height: 3rem; } + +#top-github-link { + position: relative; + z-index: 1; + float: right; + display: block; } + +#body #breadcrumbs { + height: auto; + display: block; + margin-bottom: 0; + padding-left: 0; + line-height: 1.4; } + #body #breadcrumbs span { + padding: 0 0.1rem; } + +@media only all and (max-width: 59.938em) { + #sidebar { + width: 230px; } + + #body { + margin-left: 230px; } } +@media only all and (max-width: 47.938em) { + #sidebar { + width: 230px; + left: -230px; } + + #body { + margin-left: 0; + width: 100%; } + + .sidebar-hidden { + overflow: hidden; } + .sidebar-hidden #sidebar { + left: 0; } + .sidebar-hidden #body { + margin-left: 230px; + overflow: hidden; } + .sidebar-hidden #overlay { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 10; + background: rgba(255, 255, 255, 0.5); + cursor: pointer; } } +.copy-to-clipboard { + background-image: url(../images/clippy.svg); + background-position: 50% 50%; + background-size: 16px 16px; + background-repeat: no-repeat; + width: 27px; + height: 1.45rem; + top: -1px; + display: inline-block; + vertical-align: middle; + position: relative; + color: #3c3c3c; + background-color: #f9f2f4; + margin-left: -.2rem; + cursor: pointer; + border-radius: 0 2px 2px 0; } + .copy-to-clipboard:hover { + background-color: #f1e1e5; } + pre .copy-to-clipboard { + position: absolute; + right: 4px; + top: 4px; + background-color: #eee; + border-color: #ddd; + border-radius: 2px; } + pre .copy-to-clipboard:hover { + background-color: #d9d9d9; } + +.parent-element { + -webkit-transform-style: preserve-3d; + -moz-transform-style: preserve-3d; + transform-style: preserve-3d; } + +/*# sourceMappingURL=theme.css.map */ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css.map b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css.map new file mode 100644 index 000000000..b73450315 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": ";AACQ,sFAA8E;ACStF,oCAAgB;EACf,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,GAAG;EACR,iBAAiB,EAAE,gBAAgB;EACnC,cAAc,EAAE,gBAAgB;EAChC,YAAY,EAAE,gBAAgB;EAC9B,aAAa,EAAE,gBAAgB;EAC/B,SAAS,EAAE,gBAAgB;;ACjB5B,0BAAQ;EACP,OAAO,EAAE,YAAY;EACrB,OAAO,EAAE,QAAQ;EACjB,wCAAS;IACR,MAAM,EAAE,YAAY;;ACJtB,IAAK;EACJ,UAAU,ECiBI,IAAI;EDhBlB,KAAK,ECwCY,IAAU;EDvCxB,sBAAsB,EAAE,WAAW;EACnC,uBAAuB,EAAE,SAAS;;AAGtC,CAAE;EACD,KAAK,EEPM,OAAY;EFQvB,OAAQ;IACP,KAAK,EAAE,OAAyB;;AAIlC,GAAI;EACH,QAAQ,EAAE,QAAQ;;AAGnB,GAAI;EACH,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,iBAAsB;;AAG/B,oBAAU;EACN,WAAW,EEVI,GAAG;;AFatB,uHAAmB;EGlBX,kBAAoB,EAAE,aAAM;EAK5B,eAAiB,EAAE,aAAM;EAezB,UAAY,EAAE,aAAM;;AC7B5B,QAAS;EACR,MAAM,EAAE,cAA4B;;AAGrC,kVAAyD;EACxD,gBAAgB,EAAE,KAAK;EACvB,MAAM,EAAE,cAA4B;EACpC,UAAU,EHOW,mCAAqC;EGL1D,kbAAQ;IACP,YAAY,EHAc,OAA8B;EGGzD,kbAAQ;IACP,YAAY,EFbF,OAAY;IEctB,UAAU,EHAc,oEAAwE;;AIflG,OAAQ;EACJ,UAAU,EHAF,OAAY;EGCpB,KAAK,EJEK,IAAI;EIDd,UAAU,EAAE,MAAM;EAElB,OAAO,EAAE,IAAI;EAEb,SAAE;IACE,OAAO,EAAE,YAAY;EAGzB,iBAAU;IAEN,KAAK,EHQA,IAAI;IGPT,MAAM,EHQA,IAAI;IGNV,sBAAK;MAED,IAAI,EJdF,IAAI;;AImBlB,UAAW;EACP,UAAU,EAAE,MAAM;EAClB,QAAQ,EAAE,QAAQ;EAElB,MAAM,EAAE,iBAAiC;EACzC,UAAU,EAAE,OAAqB;EACjC,aAAa,EAAE,GAAG;EAElB,gBAAM;IACF,KAAK,EAAE,wBAAiB;IACxB,QAAQ,EAAE,QAAQ;IAClB,IAAI,EAAE,IAAI;IACV,GAAG,EAAE,GAAG;EAGZ,eAAK;IACD,KAAK,EAAE,wBAAiB;IACxB,QAAQ,EAAE,QAAQ;IAClB,KAAK,EAAE,IAAI;IACX,GAAG,EAAE,GAAG;IACR,MAAM,EAAE,OAAO;IAEf,qBAAQ;MACJ,KAAK,EAAE,wBAAiB;EAIhC,gBAAM;IACF,OAAO,EAAE,YAAY;IACrB,KAAK,EJhDC,IAAI;IIiDV,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,UAAU,EAAE,WAAW;IACvB,MAAM,EAAE,CAAC;IACT,OAAO,EAAE,aAAa;IACtB,MAAM,EAAE,CAAC;IACT,WAAW,EH3CG,GAAG;IIbrB,2CAA8B;MD2DtB,KAAK,EAAE,wBAAiB;IC3DhC,kCAA8B;MD2DtB,KAAK,EAAE,wBAAiB;IC3DhC,iCAA8B;MD2DtB,KAAK,EAAE,wBAAiB;IC3DhC,sCAA8B;MD2DtB,KAAK,EAAE,wBAAiB;;AE9DpC,eAAgB;EACZ,OAAO,EAAE,IAAI;ECoBf,yCAAiE;IDrBnE,eAAgB;MAIP,OAAO,EAAE,YAAY;;AAK9B,QAAS;EAEL,gBAAgB,ELPP,OAAO;EKQhB,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,CAAC;EACN,KAAK,ELZO,KAAK;EKajB,MAAM,EAAE,CAAC;EACT,IAAI,EAAE,CAAC;EACP,WAAW,ELFM,GAAG;EKGpB,SAAS,EAAE,IAAI;EAEf,UAAE;IACE,KAAK,ELfE,OAAO;IKgBd,gBAAQ;MACJ,KAAK,EAAE,OAA2B;IAEtC,mBAAW;MACP,KAAK,EAAE,wBAAwB;EAIvC,WAAG;IACC,aAAa,EAAE,iBAAiC;EAGpD,kBAAU;IACN,OAAO,EAAE,MAAM;EAGnB,WAAG;IACC,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,QAAQ;IAClB,WAAW,EAAE,CAAC;IAEd,aAAE;MACE,OAAO,EAAE,KAAK;MACd,WAAW,EAAE,CAAC;MACd,YAAY,EAAE,CAAC;MACf,YAAY,EAAE,IAAI;MAClB,aAAa,EAAE,IAAI;IAGvB,aAAE;MACE,KAAK,EAAE,wBAAwB;MAC/B,QAAQ,EAAE,QAAQ;MAClB,KAAK,EAAE,MAAM;MACb,GAAG,EAAE,MAAM;MACX,SAAS,EAAE,GAAG;IAId,oBAAE;MACE,UAAU,EAAE,OAAuB;MACnC,KAAK,EAAE,kBAAqC;IAKhD,oBAAE;MACE,UAAU,ENhEZ,IAAI;MMiEF,KAAK,EAAE,eAAqB;IAGhC,oBAAE;MACE,KAAK,EAAE,eAAqB;EAOxC,uBAAe;IACX,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,CAAC;EAIb,8DAAY;IACR,OAAO,EAAE,KAAK;EAKtB,WAAG;IAEC,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;IAGL,sBAAE;MACE,KAAK,EAAE,OAA0B;IAIjC,oCAAE;MACE,KAAK,EAAE,OAA2B;MAClC,0CAAQ;QACJ,KAAK,EAAE,OAA2B;IAMlD,kBAAS;MACL,MAAM,EAAE,MAAM;MAGV,8BAAG;QACC,OAAO,EAAE,KAAK;MAItB,qBAAG;QACC,OAAO,EAAE,IAAI;QACb,cAAc,EAAE,IAAI;QAEpB,wBAAG;UACC,cAAc,EAAE,CAAC;MAIzB,kEAA6B;QACzB,OAAO,EAAE,KAAK;MAId,2BAAI;QACA,WAAW,EAAE,IAAI;QACjB,SAAS,EAAE,MAAM;QAEjB,6BAAE;UACE,OAAO,EAAE,GAAG;UACZ,WAAW,EAAE,MAAM;QAGvB,+BAAI;UACA,UAAU,EAAE,GAAG;MAIvB,8DAAmB;QACf,UAAU,EAAE,OAAuB;QACnC,WAAW,EAAE,KAAK;QAClB,YAAY,EAAE,KAAK;QACnB,YAAY,EAAE,IAAI;QAClB,aAAa,EAAE,IAAI;IAK/B,yBAAc;MACV,UAAU,EN7JR,IAAI;MM8JN,KAAK,EAAE,eAAqB;MAC5B,WAAW,EAAE,KAAK;MAClB,YAAY,EAAE,KAAK;MACnB,YAAY,EAAE,IAAI;MAClB,aAAa,EAAE,IAAI;IAGvB,cAAG;MACC,OAAO,EAAE,CAAC;MACV,6BAAiB;QACb,YAAY,EAAE,IAAI;MAEtB,gBAAE;QACE,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,KAAK;QACd,qBAAK;UACD,aAAa,EAAE,QAAQ;UACvB,QAAQ,EAAE,MAAM;UAChB,WAAW,EAAE,MAAM;UACnB,OAAO,EAAE,KAAK;MAGtB,kBAAI;QACA,OAAO,EAAE,KAAK;MAGlB,kBAAI;QACA,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,KAAK;QACZ,SAAS,EAAE,IAAI;QACf,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,SAAS;QACjB,UAAU,EAAE,KAAK;MAIjB,qCAAe;QACX,KAAK,ELtMb,OAAY;QKuMJ,OAAO,EAAE,MAAM;MAIvB,iBAAG;QACC,YAAY,EAAE,IAAI;QAClB,WAAW,EAAE,MAAM;;AE9MnC,KAAM;EACL,UAAU,ERiCI,OAAO;EQhCrB,MAAM,EAAE,cAAwC;;AAGjD,KAAM;EAiBF,QAAQ,EAAE,QAAQ;EAClB,WAAW,EPrBC,KAAK;EOsBjB,UAAU,EAAE,IAAI;EAlBhB,iCAAsB;IAClB,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,MAAM;IAElB,+CAAS;MACL,MAAM,EAAE,4BAA4B;MACpC,OAAO,EAAE,GAAG;IAGhB,+CAAS;MACL,UAAU,EAAE,8BAA8B;EASlD,eAAU;IACN,MAAM,EAAE,cAAc;EAG1B,cAAS;IAEL,OAAO,EAAE,SAA0B;IDRzC,yCAAkE;MCMhE,cAAS;QAKD,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,SAA0B;IDf7C,yCAAiE;MCS/D,cAAS;QAUD,OAAO,EAAE,QAAQ;EAIzB,aAAQ;IACJ,UAAU,EAAE,OAAO;IACnB,aAAa,EAAE,IAAI;EDtBzB,yCAAkE;ICyBhE,iBAAY;MAGJ,QAAQ,EAAE,MAAM;MAChB,YAAY,EAAE,YAAY;MAC1B,KAAK,EAAE,IAAI;MACX,OAAO,EAAE,KAAK;EAItB,UAAK;IAED,QAAQ,EAAE,KAAK;IACf,GAAG,EAAE,CAAC;IACN,MAAM,EAAE,CAAC;IACT,KAAK,EP9CC,IAAI;IO+CV,SAAS,EAAE,IAAI;IACf,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE,OAAO;IACf,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,MAAM;IAClB,cAAI;MACA,OAAO,EAAE,UAAU;MACnB,cAAc,EAAE,MAAM;MACtB,UAAU,EAAE,MAAM;IDjD5B,yCAAkE;MCmChE,UAAK;QAkBG,OAAO,EAAE,UAAU;QACnB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,IAAI;QACT,KAAK,EAAE,GAAG;QACV,UAAU,EAAE,MAAM;QAClB,MAAM,EAAE,KAAK;QACb,WAAW,EAAE,KAAK;QAClB,WAAW,EAAE,CAAC;QACd,cAAI;UACA,OAAO,EAAE,YAAY;IAK7B,gBAAQ;MACJ,UAAU,EPpFV,OAAO;IOuFX,mBAAW;MACP,IAAI,EAAE,CAAC;IAGX,mBAAW;MACP,KAAK,EAAE,CAAC;;AAKpB,WAAY;EACR,aAAa,EAAE,IAAI;;AAIvB,QAAS;EAEL,OAAO,EAAE,IAAI;EACb,WAAW,EAAE,MAAM;EACnB,eAAe,EAAE,MAAM;EACvB,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,MAAM;EAEf,oBAAY;IACR,cAAc,EAAE,IAAI;IACpB,SAAS,EAAE,GAAG;EAGlB,WAAG;IACC,WAAW,EZzHa,4DAA4D;IY0HpF,WAAW,EP7GG,GAAG;IO8GjB,UAAU,EAAE,MAAM;EAGtB,WAAG;IACC,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,iBAAqB;EAGxC,UAAE;IACE,UAAU,EAAE,MAAM;IAClB,SAAS,EAAE,MAAM;;AAIzB,OAAQ;EACJ,OAAO,EAAE,SAAS;EAClB,KAAK,EAAE,OAA0B;EACjC,SAAS,EAAE,IAAI;EAEf,SAAE;IACE,MAAM,EAAE,CAAC;;ACjJjB,IAAK;EACJ,WAAW,EbCoB,4DAA4D;EaAxF,cAAc,EAAE,QAAQ;EAC3B,WAAW,EAAE,GAAG;;AAIjB,sBAAuB;EACtB,WAAW,EbLoB,kEAAkE;EaMjG,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,kBAAkB;EAClC,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI;;AAGrB,EAAG;EACF,UAAU,EAAE,MAAM;EAClB,cAAc,EAAE,IAAI;;AAGrB,EAAG;EACF,cAAc,EAAE,IAAI;;AAGrB,EAAG;EACF,cAAc,EAAE,IAAI;;AAIrB,UAAW;EACV,WAAW,EAAE,kBAAsB;EACnC,YAAE;IACD,SAAS,EAAE,MAAM;IACjB,KAAK,EAAE,IAAI;EAEZ,eAAK;IACJ,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,KAAK;IACjB,KAAK,EAAE,IAAI;IACX,SAAS,EAAE,MAAM;;AAKnB,UAAW;EACP,QAAQ,EAAE,QAAQ;;AAGtB,qBAAsB;EAClB,QAAQ,EAAE,MAAM;;AAGpB,oCAAqC;EAEpC,MAAM,EAAE,CAAC;EAET,sCAAE;IACD,OAAO,EAAE,IAAI;IACb,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,IAAI;IAChB,aAAa,EAAE,IAAI;IACb,KAAK,EAAE,IAAI;IAGP,yDAAS;MACL,QAAQ,EAAE,QAAQ;MAClB,GAAG,EAAE,GAAG;MACR,KAAK,ETjEP,IAAI;MSkEF,WAAW,EAAE,WAAW;MACxB,OAAO,EAAE,GAAG;MACZ,IAAI,EAAE,IAAI;IAEd,wDAAQ;MACJ,QAAQ,EAAE,QAAQ;MAClB,GAAG,EAAE,GAAG;MACR,KAAK,ETzEP,IAAI;MS0EF,IAAI,EAAE,IAAI;MACV,WAAW,EAAE,IAAI;MACjB,OAAO,EAAE,MAAM;EAK9B,wCAAI;IAEH,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,kBAAkB;IAC9B,UAAU,EAAE,OAAO;EAGpB,qDAAiB;IAEhB,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,iCAA6B;IACzC,UAAU,EAAE,OAAO;IACb,uEAAoB;MAChB,OAAO,EAAE,SAAS;EAI7B,kEAA8B;IAE7B,WAAW,EAAE,MAAM;IACnB,UAAU,EAAE,kBAAkB;IAC9B,UAAU,EAAE,OAAO;IACb,oFAAoB;MAChB,OAAO,EAAE,MAAM;EAI1B,+EAA2C;IAE1C,WAAW,EAAE,MAAM;IACnB,UAAU,EAAE,iCAA6B;IACzC,UAAU,EAAE,OAAO;IACb,iGAAoB;MAChB,OAAO,EAAE,KAAK;;AAO1B;;;IAGK;EACJ,WAAW,Eb5HoB,wBAAwB;;Aa+HxD,IAAK;EACJ,UAAU,ETnFI,OAAO;ESoFrB,KAAK,EAAE,OAAsB;EAC7B,OAAO,EAAE,WAAW;EACnB,aAAa,EAAE,GAAG;;AAGpB,GAAI;EACH,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,MAAM;EACd,UAAU,ET1FG,OAAO;ES2FpB,MAAM,EAAE,cAA4B;EACpC,aAAa,EAAE,GAAG;EAClB,WAAW,EAAE,IAAI;EACjB,SAAS,EAAE,IAAI;EAEf,QAAK;IACJ,KAAK,ETlGS,OAAO;ISmGrB,UAAU,EAAE,OAAO;IACnB,SAAS,EAAE,IAAI;;AAKjB,EAAG;EACF,aAAa,EAAE,iBAAqB;;AAIrC,WAAY;EACX,UAAU,EAAE,KAAK;EACjB,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,IAAI;EACX,UAAU,ERrKC,OAAY;EQsKvB,KAAK,ETnKQ,IAAI;;ASwKd,mBAAc;EAAE,KAAK,EAAE,IAAI;AAC3B,yBAAoB;EAAE,KAAK,ER5KnB,OAAY;;AQgLxB,wDAAyD;EAAE,gBAAgB,EAAE,wBAAiB;;AAC9F,8CAA+C;EAAE,gBAAgB,EAAE,OAAoB;;AACvF,oDAAqD;EAAE,gBAAgB,EAAE,IAAI;;AAC7E,+DAAgE;EAAE,gBAAgB,EAAE,IAAI;;ACpLxF,KAAM;EACL,MAAM,EAAE,iBAAwC;EAC7C,YAAY,EAAE,IAAI;;AAGtB,EAAG;EAEF,UAAU,EAAE,OAA+B;EAC3C,OAAO,EAAE,MAAM;;AAGhB,EAAG;EACF,OAAO,EAAE,MAAM;EACf,MAAM,EAAE,iBAAwC;;ACbjD,OAAQ;EbSP,UAAU,EGRC,OAAY;EHSvB,KAAK,EENQ,IAAI;EFOjB,UAAU,EAAE,eAA0B;EACtC,aAAQ;IACP,UAAU,EAAE,OAAkB;IAC9B,UAAU,EAAE,eAA2B;IACvC,KAAK,EEXO,IAAI;EFajB,cAAS;IACR,UAAU,EAAE,eAA2B;;AabzC,iBAAkB;EbIjB,UAAU,EENS,OAAO;EFO1B,KAAK,EENQ,IAAI;EFOjB,UAAU,EAAE,eAA0B;EACtC,uBAAQ;IACP,UAAU,EAAE,OAAkB;IAC9B,UAAU,EAAE,eAA2B;IACvC,KAAK,EEXO,IAAI;EFajB,wBAAS;IACR,UAAU,EAAE,eAA2B;;AclBzC,QAAS;EACR,MAAM,EAAE,QAAiB;EACzB,WAAW,EAAE,QAAoB;EACjC,YAAY,EAAE,QAAoB;EAClC,QAAQ,EAAE,IAAI;;AAGf,OAAQ;EACP,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,SAAqB;;AAG/B,kBAAmB;EfUlB,KAAK,EAAE,GAAsB;EUD5B,yCAAiE;IKTnE,kBAAmB;MfUlB,KAAK,EAAE,IAAsB;;AeH9B,oBAAqB;EfGpB,KAAK,EAAE,SAAsB;EUD5B,yCAAiE;IKFnE,oBAAqB;MfGpB,KAAK,EAAE,IAAsB;;AeI9B,mBAAoB;EfJnB,KAAK,EAAE,GAAsB;EUD5B,yCAAiE;IKKnE,mBAAoB;MfJnB,KAAK,EAAE,IAAsB;;AeW9B,YAAa;EACZ,KAAK,EAAE,IAAI;EACX,UAAU,EXlCC,OAAY;EWmCvB,OAAO,EAAE,QAAqB;EAC9B,KAAK,ECrCgB,MAAM;EDsC3B,MAAM,ECtCe,MAAM;EDuC3B,aAAa,EAAE,GAAG;EAClB,KAAK,EZpCQ,IAAI;EYqCjB,SAAS,EAAE,OAAqB;EAChC,UAAU,EAAE,MAAM;;AAGnB,cAAe;EACd,UAAU,EX7CC,OAAY;;AWgDxB,cAAe;EACd,UAAU,EC/Ca,OAA6B;;ADkDrD,cAAe;EACd,UAAU,EClDa,OAA8B;;ADqDtD,eAAgB;EACf,WAAW,EAAE,OAAuB;;AEtDrC,WAAY;EACV,QAAQ,EAAE,QAAQ;;AAIpB,iBAAkB;EAChB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,OAAO;EAChB,OAAO,EAAE,IAAI;EACb,OAAO,EAAE,OAAO;EAChB,IAAI,EAAE,mFAA2C;EACjD,KAAK,EAbc,IAAI;EAcvB,UAAU,EAAE,MAAM;EAClB,eAAe,EAAE,IAAI;EACrB,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI;EACpB,cAAc,EAAE,MAAM;EACtB,SAAS,EAAE,UAAU;EACrB,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,IAAI;EACpB,OAAO,EAAE,gBAAgB;EACzB,UAAU,EAxBe,kBAAkB;EAyB3C,aAAa,EAAE,GAAG;EAClB,sBAAsB,EAAE,oBAAoB;;AAI9C,kBAAmB;EACjB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,OAAO;EAChB,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,KAAK,EApCoB,kBAAkB;EAqC3C,cAAc,EAAE,IAAI;EACpB,OAAO,EAAE,EAAE;EACX,MAAM,EAAE,qBAAqB;;AAO7B;;;;uBACQ;EACN,OAAO,EAAE,YAAY;EACrB,eAAe,EAAE,IAAI;;AAQvB;;oBAAQ;EACN,GAAG,EAAE,IAAI;EACT,KAAK,EAAE,GAAG;EACV,UAAU,EAAE,GAAG;AAGjB;;qBAAS;EACP,GAAG,EAAE,IAAI;EACT,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,IAAI;EACZ,YAAY,EAAE,IAAI;EAClB,mBAAmB,EApEI,kBAAkB;;AAyE3C,oBAAQ;EACN,KAAK,EAAE,IAAI;EACX,IAAI,EAAE,GAAG;EACT,WAAW,EAAE,KAAK;;AAItB,oBAAqB;EACnB,YAAY,EAAE,KAAK;;AAOnB;;oBAAQ;EACN,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,IAAI;EACZ,aAAa,EAAE,GAAG;AAGpB;;qBAAS;EACP,GAAG,EAAE,IAAI;EACT,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,IAAI;EACZ,YAAY,EAAE,IAAI;EAClB,gBAAgB,EAnGO,kBAAkB;;AAwG3C,oBAAQ;EACN,KAAK,EAAE,IAAI;EACX,IAAI,EAAE,GAAG;EACT,WAAW,EAAE,KAAK;;AAItB,oBAAqB;EACnB,YAAY,EAAE,KAAK;;AAIrB;mBACoB;EAClB,SAAS,EAAE,eAAe;;AAK1B,mBAAQ;EACN,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,GAAG;EACX,YAAY,EAAE,GAAG;EACjB,SAAS,EAAE,eAAe;AAG5B,oBAAS;EACP,GAAG,EAAE,GAAG;EACR,MAAM,EAAE,GAAG;EACX,IAAI,EAAE,IAAI;EACV,UAAU,EAAE,IAAI;EAChB,iBAAiB,EAvIM,kBAAkB;;AA6I3C,mBAAQ;EACN,MAAM,EAAE,GAAG;EACX,IAAI,EAAE,IAAI;EACV,WAAW,EAAE,GAAG;EAChB,SAAS,EAAE,eAAe;AAG5B,oBAAS;EACP,GAAG,EAAE,GAAG;EACR,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,GAAG;EACX,UAAU,EAAE,IAAI;EAChB,kBAAkB,EAzJK,kBAAkB;;ACD7C,oDAAoD;AAEpD,cAAe;EACX,OAAO,EAAE,WAAW;;AAGxB,eAAgB;EACZ,QAAQ,EAAE,iBAAiB;EAC3B,OAAO,EAAE,YAAY;EACrB,QAAQ,EAAE,QAAQ;;AAGtB,iCAAkC;EAC9B,MAAM,EAAE,eAAe;EACvB,UAAU,EAAE,sBAAsB;EAClC,MAAM,EAAE,IAAI;EACZ,IAAI,EAAE,CAAC;EACP,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,IAAI;EAChB,SAAS,EAAE,eAAe;EAC1B,QAAQ,EAAE,iBAAiB;EAC3B,OAAO,EAAE,CAAC;EACV,QAAQ,EAAE,mBAAmB;EAC7B,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,eAAe;;AAG1B,oDAAqD;EACjD,MAAM,EAAE,CAAC;EACT,KAAK,EAAE,CAAC;;AAGZ,eAAgB;EACZ,OAAO,EAAE,IAAI;;AAEjB,oCAAqC;EACjC,UAAU,EAAE,WAAW;;AAG3B;+CACgD;EAC5C,OAAO,EAAE,KAAK;;AAGlB;6BAC8B;EAC1B,MAAM,EAAE,OAAO;;AAMnB,kCAAmC;EAC/B,QAAQ,EAAE,iBAAiB;;AAE/B,6CAA8C;EAC1C,MAAM,EAAE,eAAe;EACvB,UAAU,EAAE,UAAU;EACtB,MAAM,EAAE,eAAe;EACvB,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,eAAe;EAC3B,SAAS,EAAE,eAAe;EAC1B,QAAQ,EAAE,iBAAiB;EAC3B,OAAO,EAAE,IAAI;EACb,OAAO,EAAE,GAAG;EACZ,QAAQ,EAAE,mBAAmB;EAC7B,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,eAAe;;AAE1B,gEAAiE;EAC7D,MAAM,EAAE,CAAC;EACT,KAAK,EAAE,CAAC;;AAMZ,wDAAwD;AAExD;sCAEA;EACI,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,EAAE;;AAGf,sCAAuC;EACnC,OAAO,EAAE,KAAK;EACd,MAAM,EAAE,IAAI;EACZ,IAAI,EAAE,CAAC;EACP,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,IAAI;;AAGf,2CAA4C;EACxC,MAAM,EAAE,GAAG;EACX,MAAM,EAAE,GAAG;EACX,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,IAAI;;AAGf,2CAA4C;EACxC,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,GAAG;EACV,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,GAAG;;AAGd,wDAAyD;EACrD,QAAQ,EAAE,MAAM;;AAGpB;;8CAE+C;EAC3C,qBAAqB,EAAE,GAAG;EAC1B,kBAAkB,EAAE,GAAG;EACvB,aAAa,EAAE,GAAG;;AAGtB;8CAC+C;EAC3C,UAAU,EAAC,qDAAqD;EAChE,MAAM,EAAE,iBAAiB;EACzB,OAAO,EAAE,GAAG;;AAIhB,yDAAyD;AAEzD,wFAAyF;EAAE,IAAI,EAAE,KAAK;;AACtG,wFAAyF;EAAE,GAAG,EAAE,KAAK;;AAGrG,uFAAwF;EAAE,IAAI,EAAE,KAAK;;AACrG,uFAAwF;EAAE,GAAG,EAAE,KAAK;;ACpInG,sBAAM;EACL,QAAQ,EAAE,OAAO;EACjB,+BAAS;IACR,QAAQ,EAAE,OAAO;;AAOnB,iBAAE;EACD,cAAc,EAAE,MAAM;;AAKxB,iBAAkB;EACd,MAAM,EAAE,YAAY;;AAQtB,yBAAG;EACF,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,QAAQ;EACjB,QAAQ,EAAE,QAAQ;AAGnB,yBAAG;EACF,MAAM,EAAE,aAAa;EACrB,OAAO,EAAE,IAAI;EACb,QAAQ,EAAE,QAAQ;EAElB,4BAAG;IAAC,WAAW,EAAE,IAAI;EAErB,+FAAO;IACN,eAAe,EAAE,SAAS;EAG3B,4BAAG;IACF,WAAW,EAAE,KAAK;AAKpB,yCAAmB;EAClB,SAAS,EAAE,IAAI;EACf,QAAQ,EAAE,QAAQ;EAClB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,QAAQ;EACjB,KAAK,EAAE,CAAC;EACR,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,IAAI;EACX,UAAU,EftCD,OAAO;AeyCjB,4BAAM;EACL,UAAU,EAAE,eAAe;EAC3B,KAAK,EAAE,eAAe;AAGvB,oCAAc;EACb,UAAU,EAAE,OAAuB;EACnC,MAAM,EAAE,iBAAoB;EAC5B,KAAK,EfhDI,OAAO;EeiDhB,uCAAG;IACF,UAAU,EfjDJ,OAAO;AesDf,oCAAe;EACd,UAAU,EAAE,OAAqB;EACjC,MAAM,EAAE,iBAAkB;EAC1B,KAAK,EfxDI,OAAO;EeyDhB,uCAAG;IACF,UAAU,EfzDA,OAAO;Ae6DnB,oCAAc;EACb,UAAU,EAAE,OAAyB;EACrC,MAAM,EAAE,iBAAsB;EAC9B,KAAK,Ef/DM,OAAO;EegElB,uCAAG;IACF,UAAU,Ef9DA,OAAO;AekEnB,oCAAa;EACZ,UAAU,EAAE,OAAyB;EACrC,MAAM,EAAE,iBAAsB;EAC9B,KAAK,EfpEQ,OAAO;EeqEpB,uCAAG;IACF,UAAU,EfvEA,OAAO;;Ae6ErB,QAAS;EACL,UAAU,EfvGF,OAAO;EewGf,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,eAAe;EACvB,OAAO,EAAE,MAAM;EACf,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,IAAI;;AAIpB,gBAAiB;EAGb,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,CAAC;EAEV,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,KAAK;;AAIlB,kBAAmB;EAIf,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,KAAK;EACd,aAAa,EAAE,CAAC;EAChB,YAAY,EAAE,CAAC;EAEf,WAAW,EAAE,GAAG;EAEhB,uBAAK;IACD,OAAO,EAAE,QAAQ;;ATvHvB,yCAAkE;ES6HhE,QAAS;IACL,KAAK,EfnJW,KAAK;;EeqJzB,KAAM;IACF,WAAW,EftJK,KAAK;AMkB3B,yCAAiE;ESwI/D,QAAS;IACL,KAAK,Ef3JW,KAAK;Ie4JrB,IAAI,EAAE,MAAwB;;EAElC,KAAM;IACF,WAAW,EAAE,CAAC;IACd,KAAK,EAAE,IAAI;;EAGf,eAAgB;IACZ,QAAQ,EAAE,MAAM;IAEhB,wBAAS;MACL,IAAI,EAAE,CAAC;IAEX,qBAAM;MACF,WAAW,Ef1KC,KAAK;Me4KjB,QAAQ,EAAE,MAAM;IAEpB,wBAAS;MACL,QAAQ,EAAE,QAAQ;MAClB,IAAI,EAAC,CAAC;MACN,KAAK,EAAE,CAAC;MACR,GAAG,EAAE,CAAC;MACN,MAAM,EAAE,CAAC;MACT,OAAO,EAAC,EAAE;MACV,UAAU,EAAE,wBAAoB;MAChC,MAAM,EAAE,OAAO;AAM3B,kBAAmB;EACjB,gBAAgB,EAAE,yBAAyB;EAC3C,mBAAmB,EAAE,OAAO;EAC5B,eAAe,EAAE,SAAS;EAC1B,iBAAiB,EAAE,SAAS;EAC5B,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,OAA6B;EACtC,GAAG,EAAE,IAAI;EACR,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,MAAM;EACtB,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,OAAsB;EAC7B,gBAAgB,EhBzJH,OAAO;EgB0JpB,WAAW,EAAE,MAAM;EACnB,MAAM,EAAE,OAAO;EACf,aAAa,EAAE,WAAW;EAE1B,wBAAQ;IACN,gBAAgB,EAAE,OAAoB;EAGxC,sBAAM;IACJ,QAAQ,EAAE,QAAQ;IAClB,KAAK,EAAE,GAAG;IACV,GAAG,EAAE,GAAG;IACR,gBAAgB,EAAE,IAAI;IACtB,YAAY,EAAE,IAAI;IAClB,aAAa,EAAE,GAAG;IAElB,4BAAQ;MACN,gBAAgB,EAAE,OAAO;;AAM/B,eAAgB;EACd,uBAAuB,EAAE,WAAW;EACpC,oBAAoB,EAAE,WAAW;EACjC,eAAe,EAAE,WAAW", +"sources": ["../scss/theme/_fonts.scss","../scss/nucleus/mixins/_utilities.scss","../scss/theme/modules/_buttons.scss","../scss/theme/_core.scss","../scss/configuration/theme/_colors.scss","../scss/theme/_configuration.scss","../scss/vendor/bourbon/addons/_prefixer.scss","../scss/theme/_forms.scss","../scss/theme/_header.scss","../scss/vendor/bourbon/css3/_placeholder.scss","../scss/theme/_nav.scss","../scss/nucleus/mixins/_breakpoints.scss","../scss/theme/_main.scss","../scss/theme/_typography.scss","../scss/theme/_tables.scss","../scss/theme/_buttons.scss","../scss/theme/_bullets.scss","../scss/configuration/theme/_bullets.scss","../scss/theme/_tooltips.scss","../scss/theme/_scrollbar.scss","../scss/theme/_custom.scss"], +"names": [], +"file": "theme.css" +} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/featherlight.min.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/featherlight.min.css new file mode 100644 index 000000000..f225bec51 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/featherlight.min.css @@ -0,0 +1,8 @@ +/** + * Featherlight - ultra slim jQuery lightbox + * Version 1.2.3 - http://noelboss.github.io/featherlight/ + * + * Copyright 2015, Noël Raoul Bossart (http://www.noelboss.com) + * MIT Licensed. +**/ +@media all{.featherlight{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:2147483647;text-align:center;white-space:nowrap;cursor:pointer;background:#333;background:rgba(0,0,0,0)}.featherlight:last-of-type{background:rgba(0,0,0,.8)}.featherlight:before{content:'';display:inline-block;height:100%;vertical-align:middle;margin-right:-.25em}.featherlight .featherlight-content{position:relative;text-align:left;vertical-align:middle;display:inline-block;overflow:auto;padding:25px 25px 0;border-bottom:25px solid transparent;min-width:30%;margin-left:5%;margin-right:5%;max-height:95%;background:#fff;cursor:auto;white-space:normal}.featherlight .featherlight-inner{display:block}.featherlight .featherlight-close-icon{position:absolute;z-index:9999;top:0;right:0;line-height:25px;width:25px;cursor:pointer;text-align:center;font:Arial,sans-serif;background:#fff;background:rgba(255,255,255,.3);color:#000}.featherlight .featherlight-image{width:100%}.featherlight-iframe .featherlight-content{border-bottom:0;padding:0}.featherlight iframe{border:0}}@media only screen and (max-width:1024px){.featherlight .featherlight-content{margin-left:10px;margin-right:10px;max-height:98%;padding:10px 10px 0;border-bottom:10px solid transparent}} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/font-awesome.min.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/font-awesome.min.css new file mode 100644 index 000000000..540440ce8 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie10.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie10.css new file mode 100644 index 000000000..3111047e9 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie10.css @@ -0,0 +1,9 @@ +button { + overflow: visible; +} + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; +} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie9.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie9.css new file mode 100644 index 000000000..46df37630 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie9.css @@ -0,0 +1,62 @@ +/* IE9 Resets and Normalization */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +audio, +canvas, +progress, +video { + display: inline-block; +} + +[hidden], +template { + display: none; +} + +abbr[title] { + border-bottom: 1px dotted; +} + +img { + border: 0; +} + +svg:not(:root) { + overflow: hidden; +} + +figure { + margin: 1em 40px; +} + +button { + overflow: visible; +} + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; +} + +legend { + border: 0; + padding: 0; +} + +textarea { + overflow: auto; +} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/pure-0.5.0/grids-min.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/pure-0.5.0/grids-min.css new file mode 100644 index 000000000..82bf81639 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/pure-0.5.0/grids-min.css @@ -0,0 +1,15 @@ +/*! +Pure v0.5.0-rc-1 +Copyright 2014 Yahoo! Inc. All rights reserved. +Licensed under the BSD License. +https://github.com/yui/pure/blob/master/LICENSE.md +*/ +.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%} + +/* Custom */ +[class *="pure-u"] {display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;} +.pure-u-1-7 {width: 14.285%;}.pure-u-2-7 {width: 28.571%;}.pure-u-3-7 {width: 42.857%;}.pure-u-4-7 {width: 57.142%;}.pure-u-5-7 {width: 71.428%;}.pure-u-6-7 {width: 85.714%;} +.pure-u-1-9 {width: 11.111%;}.pure-u-2-9 {width: 22.222%;}.pure-u-3-9 {width: 33.333%;}.pure-u-4-9 {width: 44.444%;}.pure-u-5-9 {width: 55.555%;}.pure-u-6-9 {width: 66.666%;}.pure-u-7-9 {width: 77.777%;}.pure-u-8-9 {width: 88.888%;} +.pure-u-1-10 {width: 10%;}.pure-u-2-10 {width: 20%;}.pure-u-3-10 {width: 30%;}.pure-u-4-10 {width: 40%;}.pure-u-5-10 {width: 50%;}.pure-u-6-10 {width: 60%;}.pure-u-7-10 {width: 70%;}.pure-u-8-10 {width: 80%;}.pure-u-9-10 {width: 90%;} + +.pure-u-1-11 {width: 9.090%;}.pure-u-2-11 {width: 18.181%;}.pure-u-3-11 {width: 27.272%;}.pure-u-4-11 {width: 36.363%;}.pure-u-5-11 {width: 45.454%;}.pure-u-6-11 {width: 54.545%;}.pure-u-7-11 {width: 63.636%;}.pure-u-8-11 {width: 72.727%;}.pure-u-9-11 {width: 81.818%;}.pure-u-10-11 {width: 90.909%;} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.eot b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.eot new file mode 100644 index 000000000..e9f60ca95 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.eot differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.svg b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.svg new file mode 100644 index 000000000..855c845e5 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.ttf b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.ttf new file mode 100644 index 000000000..35acda2fa Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.ttf differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..400014a4b Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff2 b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000..4d13fc604 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff2 differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/clippy.svg b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/clippy.svg new file mode 100644 index 000000000..e1b170359 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/clippy.svg @@ -0,0 +1,3 @@ + + + diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/favicon.png b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/favicon.png new file mode 100644 index 000000000..ec645f192 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/favicon.png differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/logo.png b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/logo.png new file mode 100644 index 000000000..287a4e756 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/logo.png differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/js/clipboard.min.js b/netbox/project-static/select2-4.0.12/docs/themes/learn2/js/clipboard.min.js new file mode 100644 index 000000000..000e4b48e --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/js/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v1.5.5 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Clipboard=t()}}(function(){var t,e,n;return function t(e,n,r){function o(a,c){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!c&&s)return s(a,!0);if(i)return i(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var l=n[a]={exports:{}};e[a][0].call(l.exports,function(t){var n=e[a][1][t];return o(n?n:t)},l,l.exports,t,e,n,r)}return n[a].exports}for(var i="function"==typeof require&&require,a=0;ar;r++)n[r].fn.apply(n[r].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),r=n[t],o=[];if(r&&e)for(var i=0,a=r.length;a>i;i++)r[i].fn!==e&&r[i].fn._!==e&&o.push(r[i]);return o.length?n[t]=o:delete n[t],this}},e.exports=r},{}],8:[function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}n.__esModule=!0;var i=function(){function t(t,e){for(var n=0;n0})},e=function(a,b){var c={},d=new RegExp("^"+b+"([A-Z])(.*)");for(var e in a){var f=e.match(d);if(f){var g=(f[1]+f[2].replace(/([A-Z])/g,"-$1")).toLowerCase();c[g]=a[e]}}return c},f={keyup:"onKeyUp",resize:"onResize"},g=function(c){a.each(b.opened().reverse(),function(){return c.isDefaultPrevented()||!1!==this[f[c.type]](c)?void 0:(c.preventDefault(),c.stopPropagation(),!1)})},h=function(c){if(c!==b._globalHandlerInstalled){b._globalHandlerInstalled=c;var d=a.map(f,function(a,c){return c+"."+b.prototype.namespace}).join(" ");a(window)[c?"on":"off"](d,g)}};b.prototype={constructor:b,namespace:"featherlight",targetAttr:"data-featherlight",variant:null,resetCss:!1,background:null,openTrigger:"click",closeTrigger:"click",filter:null,root:"body",openSpeed:250,closeSpeed:250,closeOnClick:"background",closeOnEsc:!0,closeIcon:"✕",loading:"",otherClose:null,beforeOpen:a.noop,beforeContent:a.noop,beforeClose:a.noop,afterOpen:a.noop,afterContent:a.noop,afterClose:a.noop,onKeyUp:a.noop,onResize:a.noop,type:null,contentFilters:["jquery","image","html","ajax","iframe","text"],setup:function(b,c){"object"!=typeof b||b instanceof a!=!1||c||(c=b,b=void 0);var d=a.extend(this,c,{target:b}),e=d.resetCss?d.namespace+"-reset":d.namespace,f=a(d.background||['
','
','',d.closeIcon,"",'
'+d.loading+"
","
","
"].join("")),g="."+d.namespace+"-close"+(d.otherClose?","+d.otherClose:"");return d.$instance=f.clone().addClass(d.variant),d.$instance.on(d.closeTrigger+"."+d.namespace,function(b){var c=a(b.target);("background"===d.closeOnClick&&c.is("."+d.namespace)||"anywhere"===d.closeOnClick||c.closest(g).length)&&(b.preventDefault(),d.close())}),this},getContent:function(){var b=this,c=this.constructor.contentFilters,d=function(a){return b.$currentTarget&&b.$currentTarget.attr(a)},e=d(b.targetAttr),f=b.target||e||"",g=c[b.type];if(!g&&f in c&&(g=c[f],f=b.target&&e),f=f||d("href")||"",!g)for(var h in c)b[h]&&(g=c[h],f=b[h]);if(!g){var i=f;if(f=null,a.each(b.contentFilters,function(){return g=c[this],g.test&&(f=g.test(i)),!f&&g.regex&&i.match&&i.match(g.regex)&&(f=i),!f}),!f)return"console"in window&&window.console.error("Featherlight: no content filter found "+(i?' for "'+i+'"':" (no target specified)")),!1}return g.process.call(b,f)},setContent:function(b){var c=this;return(b.is("iframe")||a("iframe",b).length>0)&&c.$instance.addClass(c.namespace+"-iframe"),c.$instance.removeClass(c.namespace+"-loading"),c.$instance.find("."+c.namespace+"-inner").slice(1).remove().end().replaceWith(a.contains(c.$instance[0],b[0])?"":b),c.$content=b.addClass(c.namespace+"-inner"),c},open:function(b){var d=this;if(d.$instance.hide().appendTo(d.root),!(b&&b.isDefaultPrevented()||d.beforeOpen(b)===!1)){b&&b.preventDefault();var e=d.getContent();if(e)return c.push(d),h(!0),d.$instance.fadeIn(d.openSpeed),d.beforeContent(b),a.when(e).always(function(a){d.setContent(a),d.afterContent(b)}).then(d.$instance.promise()).done(function(){d.afterOpen(b)})}return d.$instance.detach(),a.Deferred().reject().promise()},close:function(b){var c=this,e=a.Deferred();return c.beforeClose(b)===!1?e.reject():(0===d(c).length&&h(!1),c.$instance.fadeOut(c.closeSpeed,function(){c.$instance.detach(),c.afterClose(b),e.resolve()})),e.promise()},chainCallbacks:function(b){for(var c in b)this[c]=a.proxy(b[c],this,a.proxy(this[c],this))}},a.extend(b,{id:0,autoBind:"[data-featherlight]",defaults:b.prototype,contentFilters:{jquery:{regex:/^[#.]\w/,test:function(b){return b instanceof a&&b},process:function(b){return a(b).clone(!0)}},image:{regex:/\.(png|jpg|jpeg|gif|tiff|bmp)(\?\S*)?$/i,process:function(b){var c=this,d=a.Deferred(),e=new Image,f=a('');return e.onload=function(){f.naturalWidth=e.width,f.naturalHeight=e.height,d.resolve(f)},e.onerror=function(){d.reject(f)},e.src=b,d.promise()}},html:{regex:/^\s*<[\w!][^<]*>/,process:function(b){return a(b)}},ajax:{regex:/./,process:function(b){var c=a.Deferred(),d=a("
").load(b,function(a,b){"error"!==b&&c.resolve(d.contents()),c.fail()});return c.promise()}},iframe:{process:function(b){var c=new a.Deferred,d=a("