diff --git a/.gitignore b/.gitignore index 66a8b13e8..485b46d59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc +*.swp /netbox/netbox/configuration.py /netbox/netbox/ldap_config.py /netbox/reports/* @@ -6,15 +7,14 @@ /netbox/scripts/* !/netbox/scripts/__init__.py /netbox/static -.idea +/venv/ /*.sh !upgrade.sh fabfile.py -*.swp -gunicorn_config.py gunicorn.py netbox.log netbox.pid .DS_Store -.vscode +.idea .coverage +.vscode diff --git a/.travis.yml b/.travis.yml index 4ad37604e..0dcbd9ee1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ addons: language: python python: - "3.6" + - "3.7" install: - pip install -r requirements.txt - pip install pycodestyle diff --git a/README.md b/README.md index 478f37e5e..be69a9e52 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![NetBox](docs/netbox_logo.svg "NetBox logo") +**The [2020 NetBox user survey](https://docs.google.com/forms/d/1OVZuC4kQ-6kJbVf0bDB6vgkL9H96xF6phvYzby23elk/edit) is open!** Your feedback helps guide the project's long-term development. + NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically @@ -22,21 +24,25 @@ or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode | **master** | [![Build Status](https://travis-ci.org/netbox-community/netbox.svg?branch=master)](https://travis-ci.com/netbox-community/netbox/) | | **develop** | [![Build Status](https://travis-ci.org/netbox-community/netbox.svg?branch=develop)](https://travis-ci.com/netbox-community/netbox/) | -## Screenshots +### Screenshots ![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 +## Installation Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases) and run `upgrade.sh`. -# Providing Feedback +## Providing Feedback Feature requests and bug reports must be submitted as GiHub issues. (Please be sure to use the [appropriate template](https://github.com/netbox-community/netbox/issues/new/choose).) @@ -45,6 +51,6 @@ For general discussion, please consider joining our [mailing list](https://group If you are interested in contributing to the development of NetBox, please read our [contributing guide](CONTRIBUTING.md) prior to beginning any work. -# Related projects +## Related projects Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for a list of relevant community projects. diff --git a/base_requirements.txt b/base_requirements.txt index ed42b6c08..e5838ef9b 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -58,6 +58,10 @@ djangorestframework # https://github.com/axnsan12/drf-yasg drf-yasg[validation] +# WSGI HTTP server +# https://gunicorn.org/ +gunicorn + # Platform-agnostic template rendering engine # https://github.com/pallets/jinja Jinja2 @@ -79,10 +83,6 @@ Pillow # https://github.com/psycopg/psycopg2 psycopg2-binary -# GitHub-flavored Markdown extensions -# https://github.com/zopieux/py-gfm -py-gfm - # Extensive cryptographic library (fork of pycrypto) # https://github.com/Legrandin/pycryptodome pycryptodome diff --git a/contrib/netbox-rq.service b/contrib/netbox-rq.service index 7a300a195..77d70910c 100644 --- a/contrib/netbox-rq.service +++ b/contrib/netbox-rq.service @@ -7,12 +7,11 @@ Wants=network-online.target [Service] Type=simple -User=www-data -Group=www-data - +User=netbox +Group=netbox WorkingDirectory=/opt/netbox -ExecStart=/usr/bin/python3 /opt/netbox/netbox/manage.py rqworker +ExecStart=/opt/netbox/venv/bin/python3 /opt/netbox/netbox/manage.py rqworker Restart=on-failure RestartSec=30 diff --git a/contrib/netbox.service b/contrib/netbox.service index 3cc9069c6..18eb0457c 100644 --- a/contrib/netbox.service +++ b/contrib/netbox.service @@ -7,12 +7,12 @@ Wants=network-online.target [Service] Type=simple -User=www-data -Group=www-data +User=netbox +Group=netbox 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 +ExecStart=/opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi Restart=on-failure RestartSec=30 diff --git a/docs/additional-features/caching.md b/docs/additional-features/caching.md index 32ac77cbf..0e6513602 100644 --- a/docs/additional-features/caching.md +++ b/docs/additional-features/caching.md @@ -3,7 +3,7 @@ To improve performance, NetBox supports caching for most object and list views. Caching is implemented using Redis, and [django-cacheops](https://github.com/Suor/django-cacheops) -Several management commands are avaliable for administrators to manaully invalidate cache entries in extenuating circumstances. +Several management commands are avaliable for administrators to manually invalidate cache entries in extenuating circumstances. To invalidate a specifc model instance (for example a Device with ID 34): ``` diff --git a/docs/additional-features/context-data.md b/docs/additional-features/context-data.md index 465b4d2dc..432203f92 100644 --- a/docs/additional-features/context-data.md +++ b/docs/additional-features/context-data.md @@ -1,5 +1,3 @@ -# Contextual Configuration Data +# Context Data -Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object. - -Devices and Virtual Machines may also have a local config context defined. This local context will always overwrite the rendered config context objects for the Device/VM. This is useful in situations were the device requires a one-off value different from the rest of the environment. +{!docs/models/extras/configcontext.md!} diff --git a/docs/additional-features/custom-scripts.md b/docs/additional-features/custom-scripts.md index cf98a6290..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. @@ -196,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 * @@ -222,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)) @@ -240,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/napalm.md b/docs/additional-features/napalm.md index c8e8b8b3a..304d892c4 100644 --- a/docs/additional-features/napalm.md +++ b/docs/additional-features/napalm.md @@ -3,7 +3,7 @@ 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. + To enable the integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm-automation-optional) for more information. ``` GET /api/dcim/devices/1/napalm/?method=get_environment diff --git a/docs/additional-features/tags.md b/docs/additional-features/tags.md index f94957616..0609524f4 100644 --- a/docs/additional-features/tags.md +++ b/docs/additional-features/tags.md @@ -1,24 +1,3 @@ -# Tags +# Tagging -Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object (for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`. - -Each tag has a label and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters. - -Objects can be filtered by the tags they have applied. For example, the following API request will retrieve all devices tagged as "monitored": - -``` -GET /api/dcim/devices/?tag=monitored -``` - -Tags are included in the API representation of an object as a list of plain strings: - -``` -{ - ... - "tags": [ - "Core Switch", - "Monitored" - ], - ... -} -``` +{!docs/models/extras/tag.md!} 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/administration/netbox-shell.md b/docs/administration/netbox-shell.md index 2d0b8f307..bae4471b8 100644 --- a/docs/administration/netbox-shell.md +++ b/docs/administration/netbox-shell.md @@ -1,3 +1,5 @@ +# The NetBox Python Shell + NetBox includes a Python shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command: ``` diff --git a/docs/administration/replicating-netbox.md b/docs/administration/replicating-netbox.md index 781b3d214..bb7157d45 100644 --- a/docs/administration/replicating-netbox.md +++ b/docs/administration/replicating-netbox.md @@ -1,11 +1,13 @@ -# Replicating the Database +# Replicating NetBox + +## Replicating the Database NetBox uses [PostgreSQL](https://www.postgresql.org/) for its database, so general PostgreSQL best practices will apply to NetBox. You can dump and restore the database using the `pg_dump` and `psql` utilities, respectively. !!! note The examples below assume that your database is named `netbox`. -## Export the Database +### Export the Database Use the `pg_dump` utility to export the entire database to a file: @@ -19,7 +21,7 @@ When replicating a production database for development purposes, you may find it pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql ``` -## Load an Exported Database +### Load an Exported Database !!! warning This will destroy and replace any existing instance of the database. @@ -32,7 +34,7 @@ psql netbox < netbox.sql Keep in mind that PostgreSQL user accounts and permissions are not included with the dump: You will need to create those manually if you want to fully replicate the original database (see the [installation docs](../installation/1-postgresql.md)). When setting up a development instance of NetBox, it's strongly recommended to use different credentials anyway. -## Export the Database Schema +### Export the Database Schema If you want to export only the database schema, and not the data itself (e.g. for development reference), do the following: @@ -47,11 +49,11 @@ python3 manage.py invalidate all --- -# Replicating Media +## Replicating Media NetBox stored uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files. -## Archive the Media Directory +### Archive the Media Directory Execute the following command from the root of the NetBox installation path (typically `/opt/netbox`): @@ -59,7 +61,7 @@ Execute the following command from the root of the NetBox installation path (typ tar -czf netbox_media.tar.gz netbox/media/ ``` -## Restore the Media Directory +### Restore the Media Directory To extract the saved archive into a new installation, run the following from the installation root: diff --git a/docs/api/authentication.md b/docs/api/authentication.md index 08c13eea2..8e38c4de9 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -1,6 +1,8 @@ +# REST API Authentication + The NetBox API employs token-based authentication. For convenience, cookie authentication can also be used when navigating the browsable API. -# Tokens +## Tokens A token is a unique identifier that identifies a user to the API. Each user in NetBox may have one or more tokens which he or she can use to authenticate to the API. To create a token, navigate to the API tokens page at `/user/api-tokens/`. @@ -13,7 +15,7 @@ By default, a token can be used for all operations available via the API. Desele Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox. -# Authenticating to the API +## Authenticating to the API By default, read operations will be available without authentication. In this case, a token may be included in the request, but is not necessary. diff --git a/docs/api/examples.md b/docs/api/examples.md index dce088cb6..1906d0db9 100644 --- a/docs/api/examples.md +++ b/docs/api/examples.md @@ -14,7 +14,7 @@ To authenticate a request, attach your token in an `Authorization` header: curl -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" ``` -### Retrieving a list of sites +## Retrieving a list of sites Send a `GET` request to the object list endpoint. The response contains a paginated list of JSON objects. @@ -51,7 +51,7 @@ $ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/ } ``` -### Retrieving a single site by ID +## Retrieving a single site by ID Send a `GET` request to the object detail endpoint. The response contains a single JSON object. @@ -80,7 +80,7 @@ $ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/6 } ``` -### Creating a new site +## Creating a new site Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required. This example includes one non required field, "region." @@ -104,7 +104,7 @@ $ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0 ``` Note that in this example we are creating a site bound to a region with the ID of 5. For write API actions (`POST`, `PUT`, and `PATCH`) the integer ID value is used for `ForeignKey` (related model) relationships, instead of the nested representation that is used in the `GET` (list) action. -### Modify an existing site +## Modify an existing site Make an authenticated `PUT` request to the site detail endpoint. As with a create (`POST`) request, all mandatory fields must be included. @@ -112,14 +112,14 @@ Make an authenticated `PUT` request to the site detail endpoint. As with a creat $ curl -X PUT -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/ --data '{"name": "Renamed Site", "slug": "renamed-site"}' ``` -### Modify an object by changing a field +## Modify an object by changing a field Make an authenticated `PATCH` request to the device endpoint. With `PATCH`, unlike `POST` and `PUT`, we only specify the field that is being changed. In this example, we add a serial number to a device. ``` $ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/devices/2549/ --data '{"serial": "FTX1123A090"}' ``` -### Delete an existing site +## Delete an existing site Send an authenticated `DELETE` request to the site detail endpoint. diff --git a/docs/api/filtering.md b/docs/api/filtering.md new file mode 100644 index 000000000..e7b51d303 --- /dev/null +++ b/docs/api/filtering.md @@ -0,0 +1,71 @@ +# API Filtering + +The NetBox API supports robust filtering of results based on the fields of each model. +Generally speaking you are able to filter based on the attributes (fields) present in +the response body. Please note however that certain read-only or metadata fields are not +filterable. + +Filtering is achieved by passing HTTP query parameters and the parameter name is the +name of the field you wish to filter on and the value is the field value. + +E.g. filtering based on a device's name: +``` +/api/dcim/devices/?name=DC-SPINE-1 +``` + +## Multi Value Logic + +While you are able to filter based on an arbitrary number of fields, you are also able to +pass multiple values for the same field. In most cases filtering on multiple values is +implemented as a logical OR operation. A notible exception is the `tag` filter which +is a logical AND. Passing multiple values for one field, can be combined with other fields. + +For example, filtering for devices with either the name of DC-SPINE-1 _or_ DC-LEAF-4: +``` +/api/dcim/devices/?name=DC-SPINE-1&name=DC-LEAF-4 +``` + +Filtering for devices with tag `router` and `customer-a` will return only devices with +_both_ of those tags applied: +``` +/api/dcim/devices/?tag=router&tag=customer-a +``` + +## Lookup Expressions + +Certain model fields also support filtering using additonal lookup expressions. This allows +for negation and other context specific filtering. + +These lookup expressions can be applied by adding a suffix to the desired field's name. +E.g. `mac_address__n`. In this case, the filter expression is for negation and it is seperated +by two underscores. Below are the lookup expressions that are supported across different field +types. + +### Numeric Fields + +Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions: + +- `n` - not equal (negation) +- `lt` - less than +- `lte` - less than or equal +- `gt` - greater than +- `gte` - greater than or equal + +### String Fields + +String based (char) fields (Name, Address, etc) support these lookup expressions: + +- `n` - not equal (negation) +- `ic` - case insensitive contains +- `nic` - negated case insensitive contains +- `isw` - case insensitive starts with +- `nisw` - negated case insensitive starts with +- `iew` - case insensitive ends with +- `niew` - negated case insensitive ends with +- `ie` - case sensitive exact match +- `nie` - negated case sensitive exact match + +### Foreign Keys & Other Fields + +Certain other fields, namely foreign key relationships support just the negation +expression: `n`. diff --git a/docs/api/overview.md b/docs/api/overview.md index 3841e8bbf..1d8a91084 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -1,4 +1,6 @@ -# What is a REST API? +# The NetBox REST API + +## What is a REST API? REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP to create, retrieve, update, and delete objects from a database. (This set of operations is commonly referred to as CRUD.) Each type of operation is associated with a particular HTTP verb: @@ -32,11 +34,11 @@ $ curl -s http://localhost:8000/api/ipam/ip-addresses/2954/ | jq '.' Each attribute of the NetBox object is expressed as a field in the dictionary. Fields may include their own nested objects, as in the case of the `status` field above. Every object includes a primary key named `id` which uniquely identifies it in the database. -# Interactive Documentation +## Interactive Documentation Comprehensive, interactive documentation of all API endpoints is available on a running NetBox instance at `/api/docs/`. This interface provides a convenient sandbox for researching and experimenting with NetBox's various API endpoints and different request types. -# URL Hierarchy +## URL Hierarchy NetBox's entire API is housed under the API root at `https:///api/`. The URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example, the provider and circuit objects are located under the "circuits" application: @@ -62,7 +64,9 @@ Lists of objects can be filtered using a set of query parameters. For example, t GET /api/dcim/interfaces/?device_id=123 ``` -# Serialization +See [filtering](filtering.md) for more details. + +## Serialization The NetBox API employs three types of serializers to represent model data: @@ -106,7 +110,7 @@ The base serializer is used to represent the default view of a model. This inclu } ``` -## Related Objects +### Related Objects Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object. @@ -137,7 +141,7 @@ Or by a set of nested attributes used to identify the rack: Note that if the provided parameters do not return exactly one object, a validation error is raised. -## Brief Format +### Brief Format Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of the objects themselves without any related data, such as when populating a drop-down list in a form. @@ -183,7 +187,7 @@ GET /api/ipam/prefixes/13980/?brief=1 The brief format is supported for both lists and individual objects. -## Static Choice Fields +### Static Choice Fields Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL. @@ -214,7 +218,7 @@ Thus, to set a prefix's status to "Reserved," it would be assigned the integer ` A request for `GET /api/ipam/_choices/` will return choices for _all_ fields belonging to models within the IPAM app. -# Pagination +## Pagination API responses which contain a list of objects (for example, a request to `/api/dcim/devices/`) will be paginated to avoid unnecessary overhead. The root JSON object will contain the following attributes: @@ -268,7 +272,7 @@ The maximum number of objects that can be returned is limited by the [`MAX_PAGE_ !!! warning Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database. -# Filtering +## Filtering A list of objects retrieved via the API can be filtered by passing one or more query parameters. The same parameters used by the web UI work for the API as well. For example, to return only prefixes with a status of "Active" (`1`): @@ -301,7 +305,7 @@ The choices available for fixed choice fields such as `status` are exposed in th For most fields, when a filter is passed multiple times, objects matching _any_ of the provided values will be returned. For example, `GET /api/dcim/sites/?name=Foo&name=Bar` will return all sites named "Foo" _or_ "Bar". The exception to this rule is ManyToManyFields which may have multiple values assigned. Tags are the most common example of a ManyToManyField. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites tagged with both "foo" _and_ "bar". -## Custom Fields +### Custom Fields To filter on a custom field, prepend `cf_` to the field name. For example, the following query will return only sites where a custom field named `foo` is equal to 123: diff --git a/docs/api/working-with-secrets.md b/docs/api/working-with-secrets.md index 653855275..129bd0855 100644 --- a/docs/api/working-with-secrets.md +++ b/docs/api/working-with-secrets.md @@ -1,6 +1,8 @@ +# Working with Secrets + As with most other objects, the NetBox API can be used to create, modify, and delete secrets. However, additional steps are needed to encrypt or decrypt secret data. -# Generating a Session Key +## Generating a Session Key In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../../core-functionality/secrets/#user-keys). The private key must be POSTed with the name `private_key`. @@ -19,7 +21,7 @@ $ curl -X POST http://localhost:8000/api/secrets/get-session-key/ \ The request uses your private key to unlock your stored copy of the master key and generate a session key which can be attached in the `X-Session-Key` header of future API requests. -# Retrieving Secrets +## Retrieving Secrets A session key is not needed to retrieve unencrypted secrets: The secret is returned like any normal object with its `plaintext` field set to null. @@ -112,7 +114,7 @@ $ curl http://localhost:8000/api/secrets/secrets/?limit=3 \ } ``` -# Creating Secrets +## Creating Secrets Session keys are also used to decrypt new or modified secrets. This is done by setting the `plaintext` field of the submitted object: diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 619af171c..26b1aa5eb 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -4,6 +4,8 @@ NetBox's local configuration is stored in `netbox/netbox/configuration.py`. An e While NetBox has many configuration settings, only a few of them must be defined at the time of installation. +## Configuration Parameters + * [Required settings](required-settings.md) * [Optional settings](optional-settings.md) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 5b4fd1d14..dc15ed4da 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -98,6 +98,14 @@ This parameter serves as a safeguard to prevent some potentially dangerous behav --- +## DOCS_ROOT + +Default: `$INSTALL_DIR/docs/` + +The file path to NetBox's documentation. This is used when presenting context-sensitive documentation in the web UI. by default, this will be the `docs/` directory within the root NetBox installation path. (Set this to `None` to disable the embedded documentation.) + +--- + ## EMAIL In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` setting: @@ -183,6 +191,14 @@ LOGGING = { } ``` +### Available Loggers + +* `netbox.auth.*` - Authentication events +* `netbox.api.views.*` - Views which handle business logic for the REST API +* `netbox.reports.*` - Report execution (`module.name`) +* `netbox.scripts.*` - Custom script execution (`module.name`) +* `netbox.views.*` - Views which handle business logic for the web UI + --- ## LOGIN_REQUIRED diff --git a/docs/core-functionality/circuits.md b/docs/core-functionality/circuits.md index f41c94ec6..43b911308 100644 --- a/docs/core-functionality/circuits.md +++ b/docs/core-functionality/circuits.md @@ -1,34 +1,9 @@ -# Providers +# Circuits -A provider is any entity which provides some form of connectivity. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly. - -Each provider may be assigned an autonomous system number (ASN), an account number, and relevant contact information. +{!docs/models/circuits/provider.md!} --- -# Circuits - -A circuit represents a single _physical_ link connecting exactly two endpoints. (A circuit with more than two endpoints is a virtual circuit, which is not currently supported by NetBox.) Each circuit belongs to a provider and must be assigned a circuit ID which is unique to that provider. - -## Circuit Types - -Circuits are classified by type. For example, you might define circuit types for: - -* Internet transit -* Out-of-band connectivity -* Peering -* Private backhaul - -Circuit types are fully customizable. - -## Circuit Terminations - -A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites. - -Each circuit termination is tied to a site, and may optionally be connected via a cable to a specific device interface or pass-through port. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details. - -!!! note - A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit. - -!!! note - A circuit may terminate only to a physical interface. Circuits may not terminate to LAG interfaces, which are virtual interfaces: You must define each physical circuit within a service bundle separately and terminate it to its actual physical interface. +{!docs/models/circuits/circuit.md!} +{!docs/models/circuits/circuittype.md!} +{!docs/models/circuits/circuittermination.md!} diff --git a/docs/core-functionality/device-types.md b/docs/core-functionality/device-types.md new file mode 100644 index 000000000..a965f51f0 --- /dev/null +++ b/docs/core-functionality/device-types.md @@ -0,0 +1,40 @@ +# Device Types + +{!docs/models/dcim/devicetype.md!} +{!docs/models/dcim/manufacturer.md!} + +--- + +## Device Component Templates + +Each device type is assigned a number of component templates which define the physical components within a device. These are: + +* Console ports +* Console server ports +* Power ports +* Power outlets +* Network interfaces +* Front ports +* Rear ports +* Device bays (which house child devices) + +Whenever a new device is created, its components are automatically created per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates defined: + +* One template for a console port ("Console") +* Two templates for power ports ("PSU0" and "PSU1") +* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47") +* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3") + +Once component templates have been created, every new device that you create as an instance of this type will automatically be assigned each of the components listed above. + +!!! note + Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components on existing devices. + +{!docs/models/dcim/consoleporttemplate.md!} +{!docs/models/dcim/consoleserverporttemplate.md!} +{!docs/models/dcim/powerporttemplate.md!} +{!docs/models/dcim/poweroutlettemplate.md!} +{!docs/models/dcim/interfacetemplate.md!} +{!docs/models/dcim/frontporttemplate.md!} +{!docs/models/dcim/rearporttemplate.md!} +{!docs/models/dcim/devicebaytemplate.md!} diff --git a/docs/core-functionality/devices.md b/docs/core-functionality/devices.md index d170b374e..96dcf866d 100644 --- a/docs/core-functionality/devices.md +++ b/docs/core-functionality/devices.md @@ -1,152 +1,27 @@ -# Device Types +# Devices and Cabling -A device type represents a particular make and model of hardware that exists in the real world. Device types define the physical attributes of a device (rack height and depth) and its individual components (console, power, and network interfaces). - -Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type at the time of creation. (However, changes made to a device type will **not** apply to instances of that device type retroactively.) - -Some devices house child devices which share physical resources, like space and power, but which functional independently from one another. A common example of this is blade server chassis. Each device type is designated as one of the following: - -* A parent device (which has device bays) -* A child device (which must be installed in a device bay) -* Neither - -!!! note - This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. - - For that application you should create a single Device for the chassis, and add Interfaces directly to it. Interfaces can be created in bulk using range patterns, e.g. "Gi1/[1-24]". - - Add Inventory Items if you want to record the line cards themselves as separate entities. There is no explicit relationship between each interface and its line card, but it may be implied by the naming (e.g. interfaces "Gi1/x" are on line card 1) - -## Manufacturers - -Each device type must be assigned to a manufacturer. The model number of a device type must be unique to its manufacturer. - -## Component Templates - -Each device type is assigned a number of component templates which define the physical components within a device. These are: - -* Console ports -* Console server ports -* Power ports -* Power outlets -* Network interfaces -* Front ports -* Rear ports -* Device bays (which house child devices) - -Whenever a new device is created, its components are automatically created per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates defined: - -* One template for a console port ("Console") -* Two templates for power ports ("PSU0" and "PSU1") -* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47") -* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3") - -Once component templates have been created, every new device that you create as an instance of this type will automatically be assigned each of the components listed above. - -!!! note - Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components on existing devices. +{!docs/models/dcim/device.md!} +{!docs/models/dcim/devicerole.md!} +{!docs/models/dcim/platform.md!} --- -# Devices - -Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and can be half depth or full depth. A device may have a height of 0U: These devices do not consume vertical rack space and cannot be assigned to a particular rack unit. A common example of a 0U device is a vertically-mounted PDU. - -When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 is said to be mounted in U8. This logic applies to racks with both ascending and descending unit numbering. - -A device is said to be full depth if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede airflow. - ## Device Components -There are eight types of device components which comprise all of the interconnection logic with NetBox: - -* Console ports -* Console server ports -* Power ports -* Power outlets -* Network interfaces -* Front ports -* Rear ports -* Device bays - -### Console - -Console ports connect only to console server ports. Console connections can be marked as either *planned* or *connected*. - -### Power - -Power ports connect only to power outlets. Power connections can be marked as either *planned* or *connected*. - -### Interfaces - -Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. Each type of connection can be classified as either *planned* or *connected*. - -Each interface is a assigned a type denoting its physical properties. Two special types exist: the "virtual" type can be used to designate logical interfaces (such as SVIs), and the "LAG" type can be used to desinate link aggregation groups to which physical interfaces can be assigned. - -Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Fields are also provided to store an interface's MTU and MAC address. - -VLANs can be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.) - -### Pass-through Ports - -Pass-through ports are used to model physical terminations which comprise part of a longer path, such as a cable terminated to a patch panel. Each front port maps to a position on a rear port. A 24-port UTP patch panel, for instance, would have 24 front ports and 24 rear ports. Although this relationship is typically one-to-one, a rear port may have multiple front ports mapped to it. This can be useful for modeling instances where multiple paths share a common cable (for example, six different fiber connections sharing a 12-strand MPO cable). - -Pass-through ports can also be used to model "bump in the wire" devices, such as a media convertor or passive tap. - -### Device Bays - -Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations or the "Non-Racked Devices" list within the rack view. - -Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices. - -Therefore, Device bays are **not** suitable for modeling chassis-based switches and routers. These should instead be modeled as a single Device, with the line cards as Inventory Items. - -## Device Roles - -Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches. +{!docs/models/dcim/consoleport.md!} +{!docs/models/dcim/consoleserverport.md!} +{!docs/models/dcim/powerport.md!} +{!docs/models/dcim/poweroutlet.md!} +{!docs/models/dcim/interface.md!} +{!docs/models/dcim/frontport.md!} +{!docs/models/dcim/rearport.md!} +{!docs/models/dcim/devicebay.md!} +{!docs/models/dcim/inventoryitem.md!} --- -# Platforms - -A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of the same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15. - -The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. - -The assignment of platforms to devices is an optional feature, and may be disregarded if not desired. +{!docs/models/dcim/virtualchassis.md!} --- -# Inventory Items - -Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer. - ---- - -# Virtual Chassis - -A virtual chassis represents a set of devices which share a single control plane: a stack of switches which are managed as a single device, for example. Each device in the virtual chassis is assigned a position and (optionally) a priority. Exactly one device is designated the virtual chassis master: This device will typically be assigned a name, secrets, services, and other attributes related to its management. - -It's important to recognize the distinction between a virtual chassis and a chassis-based device. For instance, a virtual chassis is not used to model a chassis switch with removable line cards such as the Juniper EX9208, as its line cards are _not_ physically separate devices capable of operating independently. - ---- - -# Cables - -A cable represents a physical connection between two termination points, such as between a console port and a patch panel port, or between two network interfaces. Cables can be traced through pass-through ports to form a complete path between two endpoints. In the example below, three individual cables comprise a path between the two connected endpoints. - -``` -|<------------------------------------------ Cable Path ------------------------------------------->| - - Device A Patch Panel A Patch Panel B Device B -+-----------+ +-------------+ +-------------+ +-----------+ -| Interface | --- Cable --- | Front Port | | Front Port | --- Cable --- | Interface | -+-----------+ +-------------+ +-------------+ +-----------+ - +-------------+ +-------------+ - | Rear Port | --- Cable --- | Rear Port | - +-------------+ +-------------+ -``` - -All connections between device components in NetBox are represented using cables. However, defining the actual cable plant is optional: Components can be be directly connected using cables with no type or other attributes assigned. - -Cables are also used to associated ports and interfaces with circuit terminations. To do this, first create the circuit termination, then navigate the desired component and connect a cable between the two. +{!docs/models/dcim/cable.md!} diff --git a/docs/core-functionality/ipam.md b/docs/core-functionality/ipam.md index cd95c40e6..38572cb25 100644 --- a/docs/core-functionality/ipam.md +++ b/docs/core-functionality/ipam.md @@ -1,93 +1,16 @@ -# Aggregates +# IP Address Management -The first step to documenting your IP space is to define its scope by creating aggregates. Aggregates establish the root of your IP address hierarchy by defining the top-level allocations that you're interested in managing. Most organizations will want to track some commonly-used private IP spaces, such as: - -* 10.0.0.0/8 (RFC 1918) -* 100.64.0.0/10 (RFC 6598) -* 172.16.0.0/12 (RFC 1918) -* 192.168.0.0/16 (RFC 1918) -* One or more /48s within fd00::/8 (IPv6 unique local addressing) - -In addition to one or more of these, you'll want to create an aggregate for each globally-routable space your organization has been allocated. These aggregates should match the allocations recorded in public WHOIS databases. - -Each IP prefix will be automatically arranged under its parent aggregate if one exists. Note that it's advised to create aggregates only for IP ranges actually allocated to your organization (or marked for private use): There is no need to define aggregates for provider-assigned space which is only used on Internet circuits, for example. - -Aggregates cannot overlap with one another: They can only exist side-by-side. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix and automatically grouped under 10.0.0.0/8. Remember, the purpose of aggregates is to establish the root of your IP addressing hierarchy. - -## Regional Internet Registries (RIRs) - -[Regional Internet registries](https://en.wikipedia.org/wiki/Regional_Internet_registry) are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. There also exist lower-tier registries which serve a particular geographic area. - -Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). The RIR model includes a boolean flag which indicates whether the RIR allocates only private IP space. - -For example, suppose your organization has been allocated 104.131.0.0/16 by ARIN. It also makes use of RFC 1918 addressing internally. You would first create RIRs named ARIN and RFC 1918, then create an aggregate for each of these top-level prefixes, assigning it to its respective RIR. +{!docs/models/ipam/aggregate.md!} +{!docs/models/ipam/rir.md!} --- -# Prefixes - -A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 192.0.2.0/24). A prefix entails only the "network portion" of an IP address: All bits in the address not covered by the mask must be zero. (In other words, a prefix cannot be a specific IP address.) - -Prefixes are automatically arranged by their parent aggregates. Additionally, each prefix can be assigned to a particular site and VRF (routing table). All prefixes not assigned to a VRF will appear in the "global" table. - -Each prefix can be assigned a status and a role. These terms are often used interchangeably so it's important to recognize the difference between them. The **status** defines a prefix's operational state. Statuses are hard-coded in NetBox and can be one of the following: - -* Container - A summary of child prefixes -* Active - Provisioned and in use -* Reserved - Designated for future use -* Deprecated - No longer in use - -On the other hand, a prefix's **role** defines its function. Role assignment is optional and roles are fully customizable. For example, you might create roles to differentiate between production and development infrastructure. - -A prefix may also be assigned to a VLAN. This association is helpful for identifying which prefixes are included when reviewing a list of VLANs. - -The prefix model include a "pool" flag. If enabled, NetBox will treat this prefix as a range (such as a NAT pool) wherein every IP address is valid and assignable. This logic is used for identifying available IP addresses within a prefix. If this flag is disabled, NetBox will assume that the first and last (broadcast) address within the prefix are unusable. +{!docs/models/ipam/prefix.md!} --- -# IP Addresses - -An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world. - -Like prefixes, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs. - -Also like prefixes, each IP address can be assigned a status and a role. Statuses are hard-coded in NetBox and include the following: - -* Active -* Reserved -* Deprecated -* DHCP - -Each IP address can optionally be assigned a special role. Roles are used to indicate some special attribute of an IP address: for example, it is used as a loopback, or is a virtual IP maintained using VRRP. (Note that this differs in purpose from a _functional_ role, and thus cannot be customized.) Available roles include: - -* Loopback -* Secondary -* Anycast -* VIP -* VRRP -* HSRP -* GLBP - -An IP address can be assigned to a device or virtual machine interface, and an interface may have multiple IP addresses assigned to it. Further, each device and virtual machine may have one of its interface IPs designated as its primary IP address (one for IPv4 and one for IPv6). - -## Network Address Translation (NAT) - -An IP address can be designated as the network address translation (NAT) inside IP address for exactly one other IP address. This is useful primarily to denote a translation between public and private IP addresses. This relationship is followed in both directions: For example, if 10.0.0.1 is assigned as the inside IP for 192.0.2.1, 192.0.2.1 will be displayed as the outside IP for 10.0.0.1. - -!!! note - NetBox does not support tracking one-to-many NAT relationships (also called port address translation). This type of policy requires additional logic to model and cannot be fully represented by IP address alone. +{!docs/models/ipam/ipaddress.md!} --- -# Virtual Routing and Forwarding (VRF) - -A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space). - -Each VRF is assigned a unique name and an optional route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced. - -Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any IP prefix or address not assigned to a VRF is said to belong to the "global" table. - -By default, NetBox will allow duplicate prefixes to be assigned to a VRF. This behavior can be disabled by setting the "enforce unique" flag on the VRF model. - -!!! note - Enforcement of unique IP space can be toggled for global table (non-VRF prefixes) using the `ENFORCE_GLOBAL_UNIQUE` configuration setting. +{!docs/models/ipam/vrf.md!} diff --git a/docs/core-functionality/power.md b/docs/core-functionality/power.md index 1eda1aa00..026acd602 100644 --- a/docs/core-functionality/power.md +++ b/docs/core-functionality/power.md @@ -1,31 +1,11 @@ -# Power Panel +# Power Tracking -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. +{!docs/models/dcim/powerpanel.md!} +{!docs/models/dcim/powerfeed.md!} -# Power Feed +# Example Power Topology -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. +Below is a simple diagram demonstrating how power is modeled 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. diff --git a/docs/core-functionality/secrets.md b/docs/core-functionality/secrets.md index 515dd8d07..68771310c 100644 --- a/docs/core-functionality/secrets.md +++ b/docs/core-functionality/secrets.md @@ -1,55 +1,8 @@ # Secrets -A secret represents a single credential or other sensitive string of characters which must be stored securely. Each secret is assigned to a device within NetBox. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext. - -Each secret can also store an optional name parameter, which is not encrypted. This may be useful for storing user names. - -## Roles - -Each secret is assigned a functional role which indicates what it is used for. Secret roles are customizable. Typical roles might include: - -* Login credentials -* SNMP community strings -* RADIUS/TACACS+ keys -* IKE key strings -* Routing protocol shared secrets - -Roles are also used to control access to secrets. Each role is assigned an arbitrary number of groups and/or users. Only the users associated with a role have permission to decrypt the secrets assigned to that role. (A superuser has permission to decrypt all secrets, provided they have an active user key.) +{!docs/models/secrets/secret.md!} +{!docs/models/secrets/secretrole.md!} --- -# User Keys - -Each user within NetBox can associate his or her account with an RSA public key. If activated by an administrator, this user key will contain a unique, encrypted copy of the AES master key needed to retrieve secret data. - -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. - -To create a user key, you can either generate a new RSA key pair, or upload the public key belonging to a pair you already have. If generating a new key pair, **you must save the private key** locally before saving your new user key. Once your user key has been created, its public key will be displayed under your profile. - -When the first user key is created in NetBox, a random master encryption key is generated automatically. This key is then encrypted using the public key provided and stored as part of your user key. **The master key cannot be recovered** without your private key. - -Once a user key has been assigned an encrypted copy of the master key, it is considered activated and can now be used to encrypt and decrypt secrets. - -## Creating Additional User Keys - -Any user can create his or her user key by generating or uploading a public RSA key. However, a user key cannot be used to encrypt or decrypt secrets until it has been activated with an encrypted copy of the master key. - -Only an administrator with an active user key can activate other user keys. To do so, access the NetBox admin UI and navigate to Secrets > User Keys. Select the user key(s) to be activated, and select "activate selected user keys" from the actions dropdown. You will need to provide your private key in order to decrypt the master key. A copy of the master key is then encrypted using the public key associated with the user key being activated. +{!docs/models/secrets/userkey.md!} diff --git a/docs/core-functionality/services.md b/docs/core-functionality/services.md index 057544a91..4d4256081 100644 --- a/docs/core-functionality/services.md +++ b/docs/core-functionality/services.md @@ -1,5 +1,3 @@ -# Services +# Service Mapping -A service represents a layer four TCP or UDP service available on a device or virtual machine. For example, you might want to document that an HTTP service is running on a device. Each service includes a name, protocol, and port number; for example, "SSH (TCP/22)" or "DNS (UDP/53)." - -A service may optionally be bound to one or more specific IP addresses belonging to its parent device or VM. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.) +{!docs/models/ipam/service.md!} diff --git a/docs/core-functionality/sites-and-racks.md b/docs/core-functionality/sites-and-racks.md index f86e24b3e..073756fa1 100644 --- a/docs/core-functionality/sites-and-racks.md +++ b/docs/core-functionality/sites-and-racks.md @@ -1,51 +1,11 @@ -# Sites +# Sites and Racks -How you choose to use sites will depend on the nature of your organization, but typically a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities. - -Each site must be assigned one of the following operational statuses: - -* Active -* Planned -* Retired - -The site model provides a facility ID field which can be used to annotate a facility ID (such as a datacenter name) associated with the site. Each site may also have an autonomous system (AS) number and time zone associated with it. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.) - -The site model also includes several fields for storing contact and address information. - -## Regions - -Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned. +{!docs/models/dcim/site.md!} +{!docs/models/dcim/region.md!} --- -# Racks - -The rack model represents a physical two- or four-post equipment rack in which equipment is mounted. Each rack must be assigned to a site. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending or descending order. - -Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." A unique serial number may also be associated with each rack. - -A rack must be designated as one of the following types: - -* 2-post frame -* 4-post frame -* 4-post cabinet -* Wall-mounted frame -* Wall-mounted cabinet - -Each rack has two faces (front and rear) on which devices can be mounted. Rail-to-rail width may be 19 or 23 inches. - -## Rack Groups - -Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room. - -Each rack group must be assigned to a parent site. Hierarchical recursion of rack groups is not currently supported. - -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. - -## Rack Space Reservations - -Users can reserve units within a rack for future use. Multiple non-contiguous rack units can be associated with a single reservation (but reservations cannot span multiple racks). A rack reservation may optionally designate a specific tenant. +{!docs/models/dcim/rack.md!} +{!docs/models/dcim/rackgroup.md!} +{!docs/models/dcim/rackrole.md!} +{!docs/models/dcim/rackreservation.md!} diff --git a/docs/core-functionality/tenancy.md b/docs/core-functionality/tenancy.md index e41d4a5b6..540955698 100644 --- a/docs/core-functionality/tenancy.md +++ b/docs/core-functionality/tenancy.md @@ -1,20 +1,4 @@ -# Tenants +# Tenancy Assignment -A tenant represents a discrete entity for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. The following objects can be assigned to tenants: - -* Sites -* Racks -* Rack reservations -* Devices -* VRFs -* Prefixes -* IP addresses -* VLANs -* Circuits -* Virtual machines - -Tenant assignment is used to signify ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate. - -### Tenant Groups - -Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional. +{!docs/models/tenancy/tenant.md!} +{!docs/models/tenancy/tenantgroup.md!} diff --git a/docs/core-functionality/virtual-machines.md b/docs/core-functionality/virtual-machines.md deleted file mode 100644 index 1957c114e..000000000 --- a/docs/core-functionality/virtual-machines.md +++ /dev/null @@ -1,27 +0,0 @@ -# Clusters - -A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type, and may optionally be assigned to a group and/or site. - -Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular VM may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device. - -## Cluster Types - -A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider. - -## Cluster Groups - -Cluster groups may be created for the purpose of organizing clusters. The assignment of clusters to groups is optional. - ---- - -# Virtual Machines - -A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be associated with exactly one cluster. - -Like devices, each VM can be assigned a platform and have interfaces created on it. VM interfaces behave similarly to device interfaces, and can be assigned IP addresses, VLANs, and services. However, given their virtual nature, they cannot be connected to other interfaces. Unlike physical devices, VMs cannot be assigned console or power ports, device bays, or inventory items. - -The following resources can be defined for each VM: - -* vCPU count -* Memory (MB) -* Disk space (GB) diff --git a/docs/core-functionality/virtualization.md b/docs/core-functionality/virtualization.md new file mode 100644 index 000000000..b2bab2b7d --- /dev/null +++ b/docs/core-functionality/virtualization.md @@ -0,0 +1,9 @@ +# Virtual Machines and Clusters + +{!docs/models/virtualization/cluster.md!} +{!docs/models/virtualization/clustertype.md!} +{!docs/models/virtualization/clustergroup.md!} + +--- + +{!docs/models/virtualization/virtualmachine.md!} diff --git a/docs/core-functionality/vlans.md b/docs/core-functionality/vlans.md index a6babed44..cf1c73fe3 100644 --- a/docs/core-functionality/vlans.md +++ b/docs/core-functionality/vlans.md @@ -1,15 +1,4 @@ -# VLANs +# VLAN Management -A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site and/or VLAN group. - -Each VLAN must be assigned one of the following operational statuses: - -* Active -* Reserved -* Deprecated - -Each VLAN may also be assigned a functional role. Prefixes and VLANs share the same set of customizable roles. - -## VLAN Groups - -VLAN groups can be used to organize VLANs within NetBox. Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group. +{!docs/models/ipam/vlan.md!} +{!docs/models/ipam/vlangroup.md!} diff --git a/docs/development/extending-models.md b/docs/development/extending-models.md index dd44bb2ab..19f6ca023 100644 --- a/docs/development/extending-models.md +++ b/docs/development/extending-models.md @@ -2,7 +2,7 @@ Below is a list of items to consider when adding a new field to a model: -### 1. Generate and run database migration +## 1. Generate and run database migration Django migrations are used to express changes to the database schema. In most cases, Django can generate these automatically, however very complex changes may require manual intervention. Always remember to specify a short but descriptive name when generating a new migration. @@ -16,7 +16,7 @@ Where possible, try to merge related changes into a single migration. For exampl !!! note Migrations can only be merged within a release. Once a new release has been published, its migrations cannot be altered. -### 2. Add validation logic to `clean()` +## 2. Add validation logic to `clean()` If the new field introduces additional validation requirements (beyond what's included with the field itself), implement them in the model's `clean()` method. Remember to call the model's original method using `super()` before or agter your custom validation as appropriate: @@ -32,23 +32,23 @@ class Foo(models.Model): raise ValidationError() ``` -### 3. Add CSV helpers +## 3. Add CSV helpers Add the name of the new field to `csv_headers` and included a CSV-friendly representation of its data in the model's `to_csv()` method. These will be used when exporting objects in CSV format. -### 4. Update relevant querysets +## 4. Update relevant querysets If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retreiving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid excessive database lookups. -### 5. Update API serializer +## 5. Update API serializer Extend the model's API serializer in `.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model. -### 6. Add choices to API view +## 6. Add choices to API view If the new field has static choices, add it to the `FieldChoicesViewSet` for the app. -### 7. Add field to forms +## 7. Add field to forms Extend any forms to include the new field as appropriate. Common forms include: @@ -57,19 +57,19 @@ Extend any forms to include the new field as appropriate. Common forms include: * **CSV import** - The form used when bulk importing objects in CSV format * **Filter** - Displays the options available for filtering a list of objects (both UI and API) -### 8. Extend object filter set +## 8. Extend object filter set If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method. -### 9. Add column to object table +## 9. Add column to object table If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require explicitly declaring a new column. -### 10. Update the UI templates +## 10. Update the UI templates Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated. -### 11. Create/extend test cases +## 11. Create/extend test cases 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: diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 60be6bf61..f5244bff5 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -1,6 +1,8 @@ -# Minor Version Bumps +# Release Checklist -## Update Requirements +## Minor Version Bumps + +### Update Requirements Required Python packages are maintained in two files. `base_requirements.txt` contains a list of all the packages required by NetBox. Some of them may be pinned to a specific version of the package due to a known issue. For example: @@ -23,7 +25,7 @@ pip install -U -r base_requirements.txt 3. Run all tests and check that the UI and API function as expected. 4. Update the package versions in `requirements.txt` as appropriate. -## Update Static Libraries +### Update Static Libraries Update the following static libraries to their most recent stable release: @@ -33,41 +35,41 @@ Update the following static libraries to their most recent stable release: * jQuery * jQuery UI -## Squash Schema Migrations +### Squash Schema Migrations Database schema migrations should be squashed for each new minor release. See the [squashing guide](squashing-migrations.md) for the detailed process. -## Create a new Release Notes Page +### Create a new Release Notes Page Create a file at `/docs/release-notes/X.Y.md` to establish the release notes for the new release. Add the file to the table of contents within `mkdocs.yml`. -## Manually Perform a New Install +### Manually Perform a New Install Create a new installation of NetBox by following [the current documentation](http://netbox.readthedocs.io/en/latest/). This should be a manual process, so that issues with the documentation can be identified and corrected. -## Close the Release Milestone +### Close the Release Milestone Close the release milestone on GitHub. Ensure that there are no remaining open issues associated with it. --- -# All Releases +## All Releases -## Verify CI Build Status +### Verify CI Build Status Ensure that continuous integration testing on the `develop` branch is completing successfully. -## Update Version and Changelog +### Update Version and Changelog Update the `VERSION` constant in `settings.py` to the new release version and annotate the current data in the release notes for the new version. -## Submit a Pull Request +### Submit a Pull Request -Submit a pull request title **"Release vX.Y.X"** to merge the `develop` branch into `master`. Include a brief change log listing the features, improvements, and/or bugs addressed in the release. +Submit a pull request title **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Include a brief change log listing the features, improvements, and/or bugs addressed in the release. Once CI has completed on the PR, merge it. -## Create a New Release +### Create a New Release Draft a [new release](https://github.com/netbox-community/netbox/releases/new) with the following parameters. @@ -77,7 +79,7 @@ Draft a [new release](https://github.com/netbox-community/netbox/releases/new) w Copy the description from the pull request into the release notes. -## Update the Development Version +### Update the Development Version On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v2.3.4, set: @@ -85,6 +87,6 @@ On the `develop` branch, update `VERSION` in `settings.py` to point to the next VERSION = 'v2.3.5-dev' ``` -## Announce the Release +### Announce the Release Announce the release on the [mailing list](https://groups.google.com/forum/#!forum/netbox-discuss). Include a link to the release and the (HTML-formatted) release notes. diff --git a/docs/index.md b/docs/index.md index a68d5a6bf..4db2c55f5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ NetBox is an open source web application designed to help manage and document co * **Data circuits** - Long-haul communications circuits and providers * **Secrets** - Encrypted storage of sensitive credentials -# What NetBox Is Not +## What NetBox Is Not While NetBox strives to cover many areas of network management, the scope of its feature set is necessarily limited. This ensures that development focuses on core functionality and that scope creep is reasonably contained. To that end, it might help to provide some examples of functionality that NetBox **does not** provide: @@ -24,23 +24,23 @@ While NetBox strives to cover many areas of network management, the scope of its That said, NetBox _can_ be used to great effect in populating external tools with the data they need to perform these functions. -# Design Philosophy +## Design Philosophy NetBox was designed with the following tenets foremost in mind. -## Replicate the Real World +### Replicate the Real World Careful consideration has been given to the data model to ensure that it can accurately reflect a real-world network. For instance, IP addresses are assigned not to devices, but to specific interfaces attached to a device, and an interface may have multiple IP addresses assigned to it. -## Serve as a "Source of Truth" +### Serve as a "Source of Truth" NetBox intends to represent the _desired_ state of a network versus its _operational_ state. As such, automated import of live network state is strongly discouraged. All data created in NetBox should first be vetted by a human to ensure its integrity. NetBox can then be used to populate monitoring and provisioning systems with a high degree of confidence. -## Keep it Simple +### Keep it Simple When given a choice between a relatively simple [80% solution](https://en.wikipedia.org/wiki/Pareto_principle) and a much more complex complete solution, the former will typically be favored. This ensures a lean codebase with a low learning curve. -# Application Stack +## Application Stack NetBox is built on the [Django](https://djangoproject.com/) Python framework and utilizes a [PostgreSQL](https://www.postgresql.org/) database. It runs as a WSGI service behind your choice of HTTP server. @@ -53,6 +53,10 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and | Task queuing | Redis/django-rq | | Live device access | NAPALM | -# Getting Started +## Supported Python Versions + +NetBox supports Python 3.5, 3.6, and 3.7 environments currently. Python 3.5 is scheduled to be unsupported in NetBox v2.8. + +## Getting Started See the [installation guide](installation/index.md) for help getting NetBox up and running quickly. diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 376a62ae2..414c3c907 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -1,14 +1,15 @@ -NetBox requires a PostgreSQL database to store data. This can be hosted locally or on a remote server. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/current/static/datatype-net-types.html).) +# PostgreSQL Database Installation -!!! note - 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. +This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md). !!! warning - NetBox requires PostgreSQL 9.4 or higher. + NetBox requires PostgreSQL 9.4 or higher. Please note that MySQL and other relational databases are **not** supported. -# Installation +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. -**Ubuntu** +## Installation + +#### Ubuntu If a recent enough version of PostgreSQL is not available through your distribution's package manager, you'll need to install it from an official [PostgreSQL repository](https://wiki.postgresql.org/wiki/Apt). @@ -17,13 +18,13 @@ If a recent enough version of PostgreSQL is not available through your distribut # apt-get install -y postgresql libpq-dev ``` -**CentOS** +#### CentOS CentOS 7.5 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6. ```no-highlight -# yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm -# yum install postgresql96 postgresql96-server postgresql96-devel +# yum install -y https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm +# yum install -y postgresql96 postgresql96-server postgresql96-devel # /usr/pgsql-9.6/bin/postgresql96-setup initdb ``` @@ -41,7 +42,7 @@ Then, start the service and enable it to run at boot: # systemctl enable postgresql-9.6 ``` -# Database Creation +## Database Creation At a minimum, we need to create a database for NetBox and assign it a username and password for authentication. This is done with the following commands. @@ -62,6 +63,8 @@ GRANT postgres=# \q ``` +## Verify Service Status + You can verify that authentication works issuing the following command and providing the configured password. (Replace `localhost` with your database server if using a remote database.) ```no-highlight diff --git a/docs/installation/2-redis.md b/docs/installation/2-redis.md new file mode 100644 index 000000000..83424f156 --- /dev/null +++ b/docs/installation/2-redis.md @@ -0,0 +1,31 @@ +# Redis Installation + +## Install Redis + +[Redis](https://redis.io/) is an in-memory key-value store which NetBox employs for caching and queuing. This section entails the installation and configuration of a local Redis instance. If you already have a Redis service in place, skip to [the next section](3-netbox.md). + +### Ubuntu + +```no-highlight +# apt-get install -y redis-server +``` + +### CentOS + +```no-highlight +# yum install -y epel-release +# yum install -y redis +# systemctl start redis +# systemctl enable redis +``` + +You may wish to modify the Redis configuration at `/etc/redis.conf` or `/etc/redis/redis.conf`, however in most cases the default configuration is sufficient. + +## Verify Service Status + +Use the `redis-cli` utility to ensure the Redis service is functional: + +```no-highlight +$ redis-cli ping +PONG +``` diff --git a/docs/installation/2-netbox.md b/docs/installation/3-netbox.md similarity index 73% rename from docs/installation/2-netbox.md rename to docs/installation/3-netbox.md index cbe2c70c0..fabad20eb 100644 --- a/docs/installation/2-netbox.md +++ b/docs/installation/3-netbox.md @@ -1,25 +1,27 @@ -# Installation +# NetBox Installation This section of the documentation discusses installing and configuring the NetBox application. Begin by installing all system packages required by NetBox and its dependencies: -**Ubuntu** +## Install System Packages + +### Ubuntu ```no-highlight -# 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 +# apt-get install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev ``` -**CentOS** +### CentOS ```no-highlight -# yum install -y epel-release -# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel 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 # easy_install-3.6 pip -# ln -s /usr/bin/python3.6 /usr/bin/python3 ``` +## Download NetBox + You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub. -## Option A: Download a Release +### Option A: Download a Release Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox`. @@ -31,7 +33,7 @@ Download the [latest stable release](https://github.com/netbox-community/netbox/ # cd /opt/netbox/ ``` -## Option B: Clone the Git Repository +### Option B: Clone the Git Repository Create the base directory for the NetBox installation. For this guide, we'll use `/opt/netbox`. @@ -41,13 +43,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use If `git` is not already installed, install it: -**Ubuntu** +#### Ubuntu ```no-highlight # apt-get install -y git ``` -**CentOS** +#### CentOS ```no-highlight # yum install -y git @@ -66,45 +68,68 @@ Resolving deltas: 100% (1495/1495), done. Checking connectivity... done. ``` -!!! warning - Ensure that the media directory (`/opt/netbox/netbox/media/` in this example) and all its subdirectories are writable by the user account as which NetBox runs. If the NetBox process does not have permission to write to this directory, attempts to upload files (e.g. image attachments) will fail. (The appropriate user account will vary by platform.) +## Create the NetBox User - `# chown -R netbox:netbox /opt/netbox/netbox/media/` - -# Install Python Packages - -Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.) - -```no-highlight -# pip3 install -r requirements.txt -``` +Create a system user account named `netbox`. We'll configure the WSGI and HTTP services to run under this account. We'll also assign this user ownership of the media directory. This ensures that NetBox will be able to save local files. !!! note - If you encounter errors while installing the required packages, check that you're running a recent version of pip (v9.0.1 or higher) with the command `pip3 -V`. + CentOS users may need to create the `netbox` group first. -## NAPALM Automation (Optional) - -NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3: - -```no-highlight -# pip3 install napalm +``` +# adduser --system --group netbox +# chown --recursive netbox /opt/netbox/netbox/media/ ``` -## Remote File Storage (Optional) +## Set Up Python Environment + +We'll use a Python [virtual environment](https://docs.python.org/3.6/tutorial/venv.html) to ensure NetBox's required packages don't conflict with anything in the base system. This will create a directory named `venv` in our NetBox root. + +```no-highlight +# python3 -m venv /opt/netbox/venv +``` + +Next, activate the virtual environment and install the required Python packages. You should see your console prompt change to indicate the active environment. (Activating the virtual environment updates your command shell to use the local copy of Python that we just installed for NetBox instead of the system's Python interpreter.) + +```no-highlight +# source venv/bin/activate +(venv) # pip3 install -r requirements.txt +``` + +### NAPALM Automation (Optional) + +NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package: + +```no-highlight +(venv) # pip3 install napalm +``` + +To ensure NAPALM is automatically re-installed during future upgrades, create a file named `local_requirements.txt` in the NetBox root directory (alongside `requirements.txt`) and list the `napalm` package: + +```no-highlight +# echo napalm >> local_requirements.txt +``` + +### 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 +(venv) # pip3 install django-storages ``` -# Configuration +Don't forget to add the `django-storages` package to `local_requirements.txt` to ensure it gets re-installed during future upgrades: + +```no-highlight +# echo django-storages >> local_requirements.txt +``` + +## Configuration Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`. ```no-highlight -# cd netbox/netbox/ -# cp configuration.example.py configuration.py +(venv) # cd netbox/netbox/ +(venv) # cp configuration.example.py configuration.py ``` Open `configuration.py` with your preferred editor and set the following variables: @@ -114,7 +139,7 @@ Open `configuration.py` with your preferred editor and set the following variabl * `REDIS` * `SECRET_KEY` -## ALLOWED_HOSTS +### ALLOWED_HOSTS This is a list of the valid hostnames by which this server can be reached. You must specify at least one name or IP address. @@ -124,7 +149,7 @@ Example: ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123'] ``` -## DATABASE +### DATABASE This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, replace `localhost` with its address. See the [configuration documentation](../../configuration/required-settings/#database) for more detail on individual parameters. @@ -141,7 +166,7 @@ DATABASE = { } ``` -## REDIS +### REDIS Redis is a in-memory key-value store required as part of the NetBox installation. It is used for features such as webhooks and caching. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../../configuration/required-settings/#redis) for more detail on individual parameters. @@ -166,7 +191,7 @@ REDIS = { } ``` -## SECRET_KEY +### SECRET_KEY Generate a random secret key of at least 50 alphanumeric characters. This key must be unique to this installation and must not be shared outside the local system. @@ -175,13 +200,13 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a !!! note In the case of a highly available installation with multiple web servers, `SECRET_KEY` must be identical among all servers in order to maintain a persistent user session state. -# Run Database Migrations +## Run Database Migrations Before NetBox can run, we need to install the database schema. This is done by running `python3 manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example): ```no-highlight -# cd /opt/netbox/netbox/ -# python3 manage.py migrate +(venv) # cd /opt/netbox/netbox/ +(venv) # python3 manage.py migrate Operations to perform: Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users Running migrations: @@ -194,12 +219,12 @@ Running migrations: If this step results in a PostgreSQL authentication error, ensure that the username and password created in the database match what has been specified in `configuration.py` -# Create a Super User +## Create a Super User NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox: ```no-highlight -# python3 manage.py createsuperuser +(venv) # python3 manage.py createsuperuser Username: admin Email address: admin@example.com Password: @@ -207,20 +232,20 @@ Password (again): Superuser created successfully. ``` -# Collect Static Files +## Collect Static Files ```no-highlight -# python3 manage.py collectstatic --no-input +(venv) # python3 manage.py collectstatic --no-input 959 static files copied to '/opt/netbox/netbox/static'. ``` -# Test the Application +## Test the Application At this point, NetBox should be able to run. We can verify this by starting a development instance: ```no-highlight -# python3 manage.py runserver 0.0.0.0:8000 --insecure +(venv) # python3 manage.py runserver 0.0.0.0:8000 --insecure Performing system checks... System check identified no issues (0 silenced). diff --git a/docs/installation/3-http-daemon.md b/docs/installation/4-http-daemon.md similarity index 79% rename from docs/installation/3-http-daemon.md rename to docs/installation/4-http-daemon.md index cc1065fef..4ab28dca7 100644 --- a/docs/installation/3-http-daemon.md +++ b/docs/installation/4-http-daemon.md @@ -1,11 +1,13 @@ +# HTTP Server Setup + 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. -# Web Server Installation +## HTTP Daemon Installation -## Option A: nginx +### Option A: nginx The following will serve as a minimal nginx configuration. Be sure to modify your server name and installation path appropriately. @@ -52,7 +54,7 @@ Restart the nginx service to use the new configuration. To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04). -## Option B: Apache +### Option B: Apache ```no-highlight # apt-get install -y apache2 libapache2-mod-wsgi-py3 @@ -99,15 +101,12 @@ 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). -# gunicorn Installation +!!! 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. -Install gunicorn: +## gunicorn Configuration -```no-highlight -# pip3 install gunicorn -``` - -Copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade. +Copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade.) ```no-highlight # cd /opt/netbox @@ -116,7 +115,7 @@ Copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. We make a c You may wish to edit this file to change the bound IP address or port number, or to make performance-related adjustments. -# systemd configuration +## 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: @@ -124,17 +123,12 @@ We'll use systemd to control the daemonization of NetBox services. First, copy ` # 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. - 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 +# systemctl start netbox netbox-rq +# systemctl enable netbox netbox-rq ``` You can use the command `systemctl status netbox` to verify that the WSGI service is running: @@ -154,7 +148,20 @@ You can use the command `systemctl status netbox` to verify that the WSGI servic ... ``` -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. +At this point, you should be able to connect to the HTTP service at the server name or IP address you provided. !!! 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. + +## Troubleshooting + +If you are unable to connect to the HTTP server, check that: + +* Nginx/Apache is running and configured to listen on the correct port. +* Access is not being blocked by a firewall. (Try connecting locally from the server itself.) + +If you are able to connect but receive a 502 (bad gateway) error, check the following: + +* The NetBox system process (gunicorn) is running: `systemctl status netbox` +* nginx/Apache is configured to connect to the port on which gunicorn is listening (default is 8001). +* SELinux is not preventing the reverse proxy connection. You may need to allow HTTP network connections with the command `setsebool -P httpd_can_network_connect 1` diff --git a/docs/installation/4-ldap.md b/docs/installation/5-ldap.md similarity index 87% rename from docs/installation/4-ldap.md rename to docs/installation/5-ldap.md index 953d3cb28..b263ae040 100644 --- a/docs/installation/4-ldap.md +++ b/docs/installation/5-ldap.md @@ -1,32 +1,44 @@ +# LDAP Configuration + This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to built-in Django users in the event of a failure. -# Requirements +## Install Requirements -## Install openldap-devel +### Install System Packages On Ubuntu: ```no-highlight -sudo apt-get install -y libldap2-dev libsasl2-dev libssl-dev +# apt-get install -y libldap2-dev libsasl2-dev libssl-dev ``` On CentOS: ```no-highlight -sudo yum install -y openldap-devel +# yum install -y openldap-devel ``` -## Install django-auth-ldap +### Install django-auth-ldap + +Activate the Python virtual environment and install the `django-auth-ldap` package using pip: ```no-highlight -pip3 install django-auth-ldap +# cd /opt/netbox/ +# source venv/bin/activate +(venv) # pip3 install django-auth-ldap ``` -# Configuration +Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment: + +```no-highlight +(venv) # echo django-auth-ldap >> local_requirements.txt +``` + +## Configuration Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Complete documentation of all `django-auth-ldap` configuration options is included in the project's [official documentation](http://django-auth-ldap.readthedocs.io/). -## General Server Configuration +### General Server Configuration !!! info When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure. @@ -54,7 +66,7 @@ LDAP_IGNORE_CERT_ERRORS = True STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme. -## User Authentication +### User Authentication !!! info When using Windows Server 2012, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None. @@ -79,7 +91,7 @@ AUTH_LDAP_USER_ATTR_MAP = { } ``` -# User Groups for Permissions +### User Groups for Permissions !!! info When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. You will also need to modify the import line to use `NestedGroupOfNamesType` instead of `GroupOfNamesType` . @@ -121,7 +133,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600 !!! warning Authentication will fail if the groups (the distinguished names) do not exist in the LDAP directory. -# Troubleshooting LDAP +## Troubleshooting LDAP `supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`. diff --git a/docs/installation/index.md b/docs/installation/index.md index 59631bf7a..b3f63bcb6 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -3,14 +3,13 @@ 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 daemon](3-http-daemon.md) -4. [LDAP authentication](4-ldap.md) (optional) +1. [Redis](2-redis.md) +3. [NetBox components](3-netbox.md) +4. [HTTP daemon](4-http-daemon.md) +5. [LDAP authentication](5-ldap.md) (optional) -# Upgrading +## Upgrading 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-python3.md b/docs/installation/migrating-to-python3.md deleted file mode 100644 index 2c2c2b917..000000000 --- a/docs/installation/migrating-to-python3.md +++ /dev/null @@ -1,38 +0,0 @@ -# Migration - -!!! warning - As of version 2.5, NetBox no longer supports Python 2. Python 3 is required to run any 2.5 release or later. - -## Ubuntu - -Remove the Python2 version of gunicorn: - -```no-highlight -# pip uninstall -y gunicorn -``` - -Install Python3 and pip3, Python's package management tool: - -```no-highlight -# apt-get update -# apt-get install -y python3 python3-dev python3-setuptools -# easy_install3 pip -``` - -Install the Python3 packages required by NetBox: - -```no-highlight -# pip3 install -r requirements.txt -``` - -Replace gunicorn with the Python3 version: - -```no-highlight -# pip3 install gunicorn -``` - -If using LDAP authentication, install the `django-auth-ldap` package: - -```no-highlight -# pip3 install django-auth-ldap -``` diff --git a/docs/installation/migrating-to-systemd.md b/docs/installation/migrating-to-systemd.md index f5fcb7598..a888f18aa 100644 --- a/docs/installation/migrating-to-systemd.md +++ b/docs/installation/migrating-to-systemd.md @@ -1,16 +1,19 @@ -# Migration +# Migrating to systemd -Migration is not required, as supervisord will still continue to function. +This document contains instructions for migrating from a legacy NetBox deployment using [supervisor](http://supervisord.org/) to a systemd-based approach. ## Ubuntu -### Remove supervisord: +### Uninstall supervisord ```no-highlight # apt-get remove -y supervisord ``` -### systemd configuration: +### Configure systemd + +!!! note + These instructions assume the presence of a Python virtual environment at `/opt/netbox/venv`. If you have not created this environment, please refer to the [installation instructions](3-netbox.md#set-up-python-environment) for direction. 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: @@ -19,19 +22,14 @@ We'll use systemd to control the daemonization of NetBox services. First, copy ` ``` !!! 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. + 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 something else. 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 +# systemctl start netbox netbox-rq +# systemctl enable netbox netbox-rq ``` You can use the command `systemctl status netbox` to verify that the WSGI service is running: @@ -51,7 +49,7 @@ You can use the command `systemctl status netbox` to verify that the WSGI servic ... ``` -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. +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. Issue the command `journalctl -xe` to see why the services were unable to start. !!! 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 e5cf93a28..83cd59d1d 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -1,12 +1,14 @@ -# Review the Release Notes +# Upgrading to a New NetBox Release + +## Review the Release Notes Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../../release-notes/) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the version in which the change went into effect. -# Install the Latest Code +## Install the Latest Code As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository. -## Option A: Download a Release +### Option A: Download a Release Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`. @@ -25,31 +27,32 @@ Copy the 'configuration.py' you created when first installing to the new version # cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.py ``` +Also copy the LDAP configuration if using LDAP: + +```no-highlight +# cp netbox-X.Y.Z/netbox/netbox/ldap_config.py netbox/netbox/netbox/ldap_config.py +``` + Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.) ```no-highlight # cp -pr netbox-X.Y.Z/netbox/media/ netbox/netbox/ ``` -Also make sure to copy over any reports that you've made. Note that if you made them in a separate directory (`/opt/netbox-reports` for example), then you will not need to copy them - the config file that you copied earlier will point to the correct location. +Also make sure to copy over any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.) ```no-highlight -# cp -r /opt/netbox-X.Y.X/netbox/reports /opt/netbox/netbox/reports/ +# cp -r /opt/netbox-X.Y.Z/netbox/scripts /opt/netbox/netbox/scripts/ +# cp -r /opt/netbox-X.Y.Z/netbox/reports /opt/netbox/netbox/reports/ ``` If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well: ```no-highlight -# cp netbox-X.Y.Z/gunicorn_config.py netbox/gunicorn_config.py +# cp netbox-X.Y.Z/gunicorn.py netbox/gunicorn.py ``` -Copy the LDAP configuration if using LDAP: - -```no-highlight -# cp netbox-X.Y.Z/netbox/netbox/ldap_config.py netbox/netbox/netbox/ldap_config.py -``` - -## Option B: Clone the Git Repository (latest master release) +### Option B: Clone the Git Repository This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch: @@ -60,9 +63,9 @@ This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most # git status ``` -# Run the Upgrade Script +## Run the Upgrade Script -Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured). +Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `napalm` or `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script: ```no-highlight # ./upgrade.sh @@ -70,9 +73,14 @@ Once the new code is in place, run the upgrade script (which may need to be run This script: -* Installs or upgrades any new required Python packages +* Destroys and rebuilds the Python virtual environment +* Installs all required Python packages (listed in `requirements.txt`) +* Installs any additional packages from `local_requirements.txt` * Applies any database migrations that were included in the release * Collects all static files to be served by the HTTP service +* Deletes stale content types from the database +* Deletes all expired user sessions from the database +* Clears all cached data to prevent conflicts with the new release !!! note It's possible that the upgrade script will display a notice warning of unreflected database migrations: @@ -82,14 +90,16 @@ This script: This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are intentionally modifying the database schema. -# Restart the WSGI Service +## Restart the NetBox Services -Finally, restart the WSGI services to run the new code. If you followed this guide for the initial installation, this is done using `systemctl: +!!! warning + If you are upgrading from an installation that does not use a Python virtual environment, you'll need to update the systemd service files to reference the new Python and gunicorn executables before restarting the services. These are located in `/opt/netbox/venv/bin/`. See the example service files in `/opt/netbox/contrib/` for reference. + +Finally, restart the gunicorn and RQ services: ```no-highlight -# sudo systemctl restart netbox -# sudo systemctl restart netbox-rq +# sudo systemctl restart netbox netbox-rq ``` !!! 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. + It's possible you are still using supervisord instead of systemd. If so, please see the instructions for [migrating to systemd](migrating-to-systemd.md). 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/models/circuits/circuit.md b/docs/models/circuits/circuit.md new file mode 100644 index 000000000..47320495d --- /dev/null +++ b/docs/models/circuits/circuit.md @@ -0,0 +1,3 @@ +# Circuits + +A circuit represents a single _physical_ link connecting exactly two endpoints. (A circuit with more than two endpoints is a virtual circuit, which is not currently supported by NetBox.) Each circuit belongs to a provider and must be assigned a circuit ID which is unique to that provider. diff --git a/docs/models/circuits/circuittermination.md b/docs/models/circuits/circuittermination.md new file mode 100644 index 000000000..a39236314 --- /dev/null +++ b/docs/models/circuits/circuittermination.md @@ -0,0 +1,11 @@ +# Circuit Terminations + +A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites. + +Each circuit termination is tied to a site, and may optionally be connected via a cable to a specific device interface or pass-through port. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details. + +!!! note + A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit. + +!!! note + A circuit may terminate only to a physical interface. Circuits may not terminate to LAG interfaces, which are virtual interfaces: You must define each physical circuit within a service bundle separately and terminate it to its actual physical interface. diff --git a/docs/models/circuits/circuittype.md b/docs/models/circuits/circuittype.md new file mode 100644 index 000000000..a9ae117b8 --- /dev/null +++ b/docs/models/circuits/circuittype.md @@ -0,0 +1,10 @@ +# Circuit Types + +Circuits are classified by type. For example, you might define circuit types for: + +* Internet transit +* Out-of-band connectivity +* Peering +* Private backhaul + +Circuit types are fully customizable. \ No newline at end of file diff --git a/docs/models/circuits/provider.md b/docs/models/circuits/provider.md new file mode 100644 index 000000000..c8e19f6cd --- /dev/null +++ b/docs/models/circuits/provider.md @@ -0,0 +1,5 @@ +# Providers + +A provider is any entity which provides some form of connectivity. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly. + +Each provider may be assigned an autonomous system number (ASN), an account number, and relevant contact information. diff --git a/docs/models/dcim/cable.md b/docs/models/dcim/cable.md new file mode 100644 index 000000000..8b00a999d --- /dev/null +++ b/docs/models/dcim/cable.md @@ -0,0 +1,19 @@ +# Cables + +A cable represents a physical connection between two termination points, such as between a console port and a patch panel port, or between two network interfaces. Cables can be traced through pass-through ports to form a complete path between two endpoints. In the example below, three individual cables comprise a path between the two connected endpoints. + +``` +|<------------------------------------------ Cable Path ------------------------------------------->| + + Device A Patch Panel A Patch Panel B Device B ++-----------+ +-------------+ +-------------+ +-----------+ +| Interface | --- Cable --- | Front Port | | Front Port | --- Cable --- | Interface | ++-----------+ +-------------+ +-------------+ +-----------+ + +-------------+ +-------------+ + | Rear Port | --- Cable --- | Rear Port | + +-------------+ +-------------+ +``` + +All connections between device components in NetBox are represented using cables. However, defining the actual cable plant is optional: Components can be be directly connected using cables with no type or other attributes assigned. + +Cables are also used to associated ports and interfaces with circuit terminations. To do this, first create the circuit termination, then navigate the desired component and connect a cable between the two. diff --git a/docs/models/dcim/consoleport.md b/docs/models/dcim/consoleport.md new file mode 100644 index 000000000..4d3a089c5 --- /dev/null +++ b/docs/models/dcim/consoleport.md @@ -0,0 +1,5 @@ +## Console Ports + +A console port provides connectivity to the physical console of a device. Console ports are typically used for temporary access by someone who is physically near the device, or for remote out-of-band access via a console server. + +Console ports can be connected to console server ports. diff --git a/docs/models/dcim/consoleporttemplate.md b/docs/models/dcim/consoleporttemplate.md new file mode 100644 index 000000000..86281cb92 --- /dev/null +++ b/docs/models/dcim/consoleporttemplate.md @@ -0,0 +1,3 @@ +## Console Port Templates + +A template for a console port that will be created on all instantiations of the parent device type. diff --git a/docs/models/dcim/consoleserverport.md b/docs/models/dcim/consoleserverport.md new file mode 100644 index 000000000..55aefd733 --- /dev/null +++ b/docs/models/dcim/consoleserverport.md @@ -0,0 +1,5 @@ +## Console Server Ports + +A console server is a device which provides remote access to the local consoles of connected devices. This is typically done to provide remote out-of-band access to network devices. + +Console server ports can be connected to console ports. diff --git a/docs/models/dcim/consoleserverporttemplate.md b/docs/models/dcim/consoleserverporttemplate.md new file mode 100644 index 000000000..ed99adb11 --- /dev/null +++ b/docs/models/dcim/consoleserverporttemplate.md @@ -0,0 +1,3 @@ +## Console Server Port Templates + +A template for a console server port that will be created on all instantiations of the parent device type. diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md new file mode 100644 index 000000000..9ec2875da --- /dev/null +++ b/docs/models/dcim/device.md @@ -0,0 +1,7 @@ +# Devices + +Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and can be half depth or full depth. A device may have a height of 0U: These devices do not consume vertical rack space and cannot be assigned to a particular rack unit. A common example of a 0U device is a vertically-mounted PDU. + +When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 is said to be mounted in U8. This logic applies to racks with both ascending and descending unit numbering. + +A device is said to be full depth if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede airflow. diff --git a/docs/models/dcim/devicebay.md b/docs/models/dcim/devicebay.md new file mode 100644 index 000000000..cdcd5657d --- /dev/null +++ b/docs/models/dcim/devicebay.md @@ -0,0 +1,7 @@ +## Device Bays + +Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations or the "Non-Racked Devices" list within the rack view. + +Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices. + +Therefore, Device bays are **not** suitable for modeling chassis-based switches and routers. These should instead be modeled as a single Device, with the line cards as Inventory Items. diff --git a/docs/models/dcim/devicebaytemplate.md b/docs/models/dcim/devicebaytemplate.md new file mode 100644 index 000000000..ebf7bd63c --- /dev/null +++ b/docs/models/dcim/devicebaytemplate.md @@ -0,0 +1,3 @@ +## Device Bay Templates + +A template for a device bay that will be created on all instantiations of the parent device type. diff --git a/docs/models/dcim/devicerole.md b/docs/models/dcim/devicerole.md new file mode 100644 index 000000000..315f81356 --- /dev/null +++ b/docs/models/dcim/devicerole.md @@ -0,0 +1,3 @@ +# Device Roles + +Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches. diff --git a/docs/models/dcim/devicetype.md b/docs/models/dcim/devicetype.md new file mode 100644 index 000000000..1a10cee41 --- /dev/null +++ b/docs/models/dcim/devicetype.md @@ -0,0 +1,18 @@ +# Device Types + +A device type represents a particular make and model of hardware that exists in the real world. Device types define the physical attributes of a device (rack height and depth) and its individual components (console, power, and network interfaces). + +Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type at the time of creation. (However, changes made to a device type will **not** apply to instances of that device type retroactively.) + +Some devices house child devices which share physical resources, like space and power, but which functional independently from one another. A common example of this is blade server chassis. Each device type is designated as one of the following: + +* A parent device (which has device bays) +* A child device (which must be installed in a device bay) +* Neither + +!!! note + This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. + + For that application you should create a single Device for the chassis, and add Interfaces directly to it. Interfaces can be created in bulk using range patterns, e.g. "Gi1/[1-24]". + + Add Inventory Items if you want to record the line cards themselves as separate entities. There is no explicit relationship between each interface and its line card, but it may be implied by the naming (e.g. interfaces "Gi1/x" are on line card 1) diff --git a/docs/models/dcim/frontport.md b/docs/models/dcim/frontport.md new file mode 100644 index 000000000..12b9cfc16 --- /dev/null +++ b/docs/models/dcim/frontport.md @@ -0,0 +1,5 @@ +## Front Ports + +Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. + +Each front port is mapped to a specific rear port on the same device. A single rear port may be mapped to multiple rear ports. \ No newline at end of file diff --git a/docs/models/dcim/frontporttemplate.md b/docs/models/dcim/frontporttemplate.md new file mode 100644 index 000000000..b32349519 --- /dev/null +++ b/docs/models/dcim/frontporttemplate.md @@ -0,0 +1,3 @@ +## Front Port Templates + +A template for a front-facing pass-through port that will be created on all instantiations of the parent device type. diff --git a/docs/models/dcim/interface.md b/docs/models/dcim/interface.md new file mode 100644 index 000000000..cbccbec8d --- /dev/null +++ b/docs/models/dcim/interface.md @@ -0,0 +1,9 @@ +## Interfaces + +Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. Each type of connection can be classified as either *planned* or *connected*. + +Each interface is a assigned a type denoting its physical properties. Two special types exist: the "virtual" type can be used to designate logical interfaces (such as SVIs), and the "LAG" type can be used to desinate link aggregation groups to which physical interfaces can be assigned. + +Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Fields are also provided to store an interface's MTU and MAC address. + +VLANs can be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.) diff --git a/docs/models/dcim/interfacetemplate.md b/docs/models/dcim/interfacetemplate.md new file mode 100644 index 000000000..07fc3a65b --- /dev/null +++ b/docs/models/dcim/interfacetemplate.md @@ -0,0 +1,3 @@ +## Interface Templates + +A template for an interface that will be created on all instantiations of the parent device type. diff --git a/docs/models/dcim/inventoryitem.md b/docs/models/dcim/inventoryitem.md new file mode 100644 index 000000000..b113dce1e --- /dev/null +++ b/docs/models/dcim/inventoryitem.md @@ -0,0 +1,3 @@ +# Inventory Items + +Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer. diff --git a/docs/models/dcim/manufacturer.md b/docs/models/dcim/manufacturer.md new file mode 100644 index 000000000..cee89291d --- /dev/null +++ b/docs/models/dcim/manufacturer.md @@ -0,0 +1,3 @@ +# Manufacturers + +Each device type must be assigned to a manufacturer. The model number of a device type must be unique to its manufacturer. diff --git a/docs/models/dcim/platform.md b/docs/models/dcim/platform.md new file mode 100644 index 000000000..19528da13 --- /dev/null +++ b/docs/models/dcim/platform.md @@ -0,0 +1,7 @@ +# Platforms + +A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of the same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15. + +The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. + +The assignment of platforms to devices is an optional feature, and may be disregarded if not desired. diff --git a/docs/models/dcim/powerfeed.md b/docs/models/dcim/powerfeed.md new file mode 100644 index 000000000..ab8621e14 --- /dev/null +++ b/docs/models/dcim/powerfeed.md @@ -0,0 +1,8 @@ +# 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. diff --git a/docs/models/dcim/poweroutlet.md b/docs/models/dcim/poweroutlet.md new file mode 100644 index 000000000..0ec93856e --- /dev/null +++ b/docs/models/dcim/poweroutlet.md @@ -0,0 +1,3 @@ +## Power Outlets + +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. diff --git a/docs/models/dcim/poweroutlettemplate.md b/docs/models/dcim/poweroutlettemplate.md new file mode 100644 index 000000000..e5b54af23 --- /dev/null +++ b/docs/models/dcim/poweroutlettemplate.md @@ -0,0 +1,3 @@ +## Power Outlet Templates + +A template for a power outlet that will be created on all instantiations of the parent device type. diff --git a/docs/models/dcim/powerpanel.md b/docs/models/dcim/powerpanel.md new file mode 100644 index 000000000..3b05f8fad --- /dev/null +++ b/docs/models/dcim/powerpanel.md @@ -0,0 +1,3 @@ +# 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. diff --git a/docs/models/dcim/powerport.md b/docs/models/dcim/powerport.md new file mode 100644 index 000000000..6027fa98b --- /dev/null +++ b/docs/models/dcim/powerport.md @@ -0,0 +1,6 @@ +## Power Ports + +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. diff --git a/docs/models/dcim/powerporttemplate.md b/docs/models/dcim/powerporttemplate.md new file mode 100644 index 000000000..b6e64be01 --- /dev/null +++ b/docs/models/dcim/powerporttemplate.md @@ -0,0 +1,3 @@ +## Power Port Templates + +A template for a power port that will be created on all instantiations of the parent device type. diff --git a/docs/models/dcim/rack.md b/docs/models/dcim/rack.md new file mode 100644 index 000000000..39858b823 --- /dev/null +++ b/docs/models/dcim/rack.md @@ -0,0 +1,15 @@ +# Racks + +The rack model represents a physical two- or four-post equipment rack in which equipment is mounted. Each rack must be assigned to a site. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending or descending order. + +Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." A unique serial number may also be associated with each rack. + +A rack must be designated as one of the following types: + +* 2-post frame +* 4-post frame +* 4-post cabinet +* Wall-mounted frame +* Wall-mounted cabinet + +Each rack has two faces (front and rear) on which devices can be mounted. Rail-to-rail width may be 19 or 23 inches. diff --git a/docs/models/dcim/rackgroup.md b/docs/models/dcim/rackgroup.md new file mode 100644 index 000000000..ad9df4eef --- /dev/null +++ b/docs/models/dcim/rackgroup.md @@ -0,0 +1,7 @@ +# Rack Groups + +Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room. + +Each rack group must be assigned to a parent site. Hierarchical recursion of rack groups is not currently supported. + +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.) diff --git a/docs/models/dcim/rackreservation.md b/docs/models/dcim/rackreservation.md new file mode 100644 index 000000000..09de55553 --- /dev/null +++ b/docs/models/dcim/rackreservation.md @@ -0,0 +1,3 @@ +# Rack Reservations + +Users can reserve units within a rack for future use. Multiple non-contiguous rack units can be associated with a single reservation (but reservations cannot span multiple racks). A rack reservation may optionally designate a specific tenant. diff --git a/docs/models/dcim/rackrole.md b/docs/models/dcim/rackrole.md new file mode 100644 index 000000000..63e9c1469 --- /dev/null +++ b/docs/models/dcim/rackrole.md @@ -0,0 +1,3 @@ +# 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/models/dcim/rearport.md b/docs/models/dcim/rearport.md new file mode 100644 index 000000000..8c8136338 --- /dev/null +++ b/docs/models/dcim/rearport.md @@ -0,0 +1,5 @@ +## Rear Ports + +Like front ports, rear ports are pass-through ports which represent the end of a particular cable segment in a path. Each rear port is defined with a number of positions: rear ports with more than one position can be mapped to multiple front ports. This can be useful for modeling instances where multiple paths share a common cable (for example, six different fiber connections sharing a 12-strand MPO cable). + +Note that front and rear ports need not necessarily reside on the actual front or rear device face. This terminology is used primarily to distinguish between the two components in a pass-through port pairing. diff --git a/docs/models/dcim/rearporttemplate.md b/docs/models/dcim/rearporttemplate.md new file mode 100644 index 000000000..448c0befd --- /dev/null +++ b/docs/models/dcim/rearporttemplate.md @@ -0,0 +1,3 @@ +## Rear Port Templates + +A template for a rear-facing pass-through port that will be created on all instantiations of the parent device type. diff --git a/docs/models/dcim/region.md b/docs/models/dcim/region.md new file mode 100644 index 000000000..734467500 --- /dev/null +++ b/docs/models/dcim/region.md @@ -0,0 +1,3 @@ +# Regions + +Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned. diff --git a/docs/models/dcim/site.md b/docs/models/dcim/site.md new file mode 100644 index 000000000..b13056a99 --- /dev/null +++ b/docs/models/dcim/site.md @@ -0,0 +1,13 @@ +# Sites + +How you choose to use sites will depend on the nature of your organization, but typically a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities. + +Each site must be assigned one of the following operational statuses: + +* Active +* Planned +* Retired + +The site model provides a facility ID field which can be used to annotate a facility ID (such as a datacenter name) associated with the site. Each site may also have an autonomous system (AS) number and time zone associated with it. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.) + +The site model also includes several fields for storing contact and address information. diff --git a/docs/models/dcim/virtualchassis.md b/docs/models/dcim/virtualchassis.md new file mode 100644 index 000000000..e1707918b --- /dev/null +++ b/docs/models/dcim/virtualchassis.md @@ -0,0 +1,5 @@ +# Virtual Chassis + +A virtual chassis represents a set of devices which share a single control plane: a stack of switches which are managed as a single device, for example. Each device in the virtual chassis is assigned a position and (optionally) a priority. Exactly one device is designated the virtual chassis master: This device will typically be assigned a name, secrets, services, and other attributes related to its management. + +It's important to recognize the distinction between a virtual chassis and a chassis-based device. For instance, a virtual chassis is not used to model a chassis switch with removable line cards such as the Juniper EX9208, as its line cards are _not_ physically separate devices capable of operating independently. diff --git a/docs/models/extras/configcontext.md b/docs/models/extras/configcontext.md new file mode 100644 index 000000000..380e631d8 --- /dev/null +++ b/docs/models/extras/configcontext.md @@ -0,0 +1,5 @@ +# Configuration Contexts + +Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object. + +Devices and Virtual Machines may also have a local config context defined. This local context will always overwrite the rendered config context objects for the Device/VM. This is useful in situations were the device requires a one-off value different from the rest of the environment. diff --git a/docs/models/extras/tag.md b/docs/models/extras/tag.md new file mode 100644 index 000000000..f94957616 --- /dev/null +++ b/docs/models/extras/tag.md @@ -0,0 +1,24 @@ +# Tags + +Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object (for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`. + +Each tag has a label and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters. + +Objects can be filtered by the tags they have applied. For example, the following API request will retrieve all devices tagged as "monitored": + +``` +GET /api/dcim/devices/?tag=monitored +``` + +Tags are included in the API representation of an object as a list of plain strings: + +``` +{ + ... + "tags": [ + "Core Switch", + "Monitored" + ], + ... +} +``` diff --git a/docs/models/ipam/aggregate.md b/docs/models/ipam/aggregate.md new file mode 100644 index 000000000..f43209619 --- /dev/null +++ b/docs/models/ipam/aggregate.md @@ -0,0 +1,15 @@ +# Aggregates + +The first step to documenting your IP space is to define its scope by creating aggregates. Aggregates establish the root of your IP address hierarchy by defining the top-level allocations that you're interested in managing. Most organizations will want to track some commonly-used private IP spaces, such as: + +* 10.0.0.0/8 (RFC 1918) +* 100.64.0.0/10 (RFC 6598) +* 172.16.0.0/12 (RFC 1918) +* 192.168.0.0/16 (RFC 1918) +* One or more /48s within fd00::/8 (IPv6 unique local addressing) + +In addition to one or more of these, you'll want to create an aggregate for each globally-routable space your organization has been allocated. These aggregates should match the allocations recorded in public WHOIS databases. + +Each IP prefix will be automatically arranged under its parent aggregate if one exists. Note that it's advised to create aggregates only for IP ranges actually allocated to your organization (or marked for private use): There is no need to define aggregates for provider-assigned space which is only used on Internet circuits, for example. + +Aggregates cannot overlap with one another: They can only exist side-by-side. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix and automatically grouped under 10.0.0.0/8. Remember, the purpose of aggregates is to establish the root of your IP addressing hierarchy. diff --git a/docs/models/ipam/ipaddress.md b/docs/models/ipam/ipaddress.md new file mode 100644 index 000000000..cbe12553d --- /dev/null +++ b/docs/models/ipam/ipaddress.md @@ -0,0 +1,31 @@ +# IP Addresses + +An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world. + +Like prefixes, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs. + +Also like prefixes, each IP address can be assigned a status and a role. Statuses are hard-coded in NetBox and include the following: + +* Active +* Reserved +* Deprecated +* DHCP + +Each IP address can optionally be assigned a special role. Roles are used to indicate some special attribute of an IP address: for example, it is used as a loopback, or is a virtual IP maintained using VRRP. (Note that this differs in purpose from a _functional_ role, and thus cannot be customized.) Available roles include: + +* Loopback +* Secondary +* Anycast +* VIP +* VRRP +* HSRP +* GLBP + +An IP address can be assigned to a device or virtual machine interface, and an interface may have multiple IP addresses assigned to it. Further, each device and virtual machine may have one of its interface IPs designated as its primary IP address (one for IPv4 and one for IPv6). + +## Network Address Translation (NAT) + +An IP address can be designated as the network address translation (NAT) inside IP address for exactly one other IP address. This is useful primarily to denote a translation between public and private IP addresses. This relationship is followed in both directions: For example, if 10.0.0.1 is assigned as the inside IP for 192.0.2.1, 192.0.2.1 will be displayed as the outside IP for 10.0.0.1. + +!!! note + NetBox does not support tracking one-to-many NAT relationships (also called port address translation). This type of policy requires additional logic to model and cannot be fully represented by IP address alone. diff --git a/docs/models/ipam/prefix.md b/docs/models/ipam/prefix.md new file mode 100644 index 000000000..9ab5382a5 --- /dev/null +++ b/docs/models/ipam/prefix.md @@ -0,0 +1,18 @@ +# Prefixes + +A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 192.0.2.0/24). A prefix entails only the "network portion" of an IP address: All bits in the address not covered by the mask must be zero. (In other words, a prefix cannot be a specific IP address.) + +Prefixes are automatically arranged by their parent aggregates. Additionally, each prefix can be assigned to a particular site and VRF (routing table). All prefixes not assigned to a VRF will appear in the "global" table. + +Each prefix can be assigned a status and a role. These terms are often used interchangeably so it's important to recognize the difference between them. The **status** defines a prefix's operational state. Statuses are hard-coded in NetBox and can be one of the following: + +* Container - A summary of child prefixes +* Active - Provisioned and in use +* Reserved - Designated for future use +* Deprecated - No longer in use + +On the other hand, a prefix's **role** defines its function. Role assignment is optional and roles are fully customizable. For example, you might create roles to differentiate between production and development infrastructure. + +A prefix may also be assigned to a VLAN. This association is helpful for identifying which prefixes are included when reviewing a list of VLANs. + +The prefix model include a "pool" flag. If enabled, NetBox will treat this prefix as a range (such as a NAT pool) wherein every IP address is valid and assignable. This logic is used for identifying available IP addresses within a prefix. If this flag is disabled, NetBox will assume that the first and last (broadcast) address within the prefix are unusable. diff --git a/docs/models/ipam/rir.md b/docs/models/ipam/rir.md new file mode 100644 index 000000000..69c34e72d --- /dev/null +++ b/docs/models/ipam/rir.md @@ -0,0 +1,7 @@ +# Regional Internet Registries (RIRs) + +[Regional Internet registries](https://en.wikipedia.org/wiki/Regional_Internet_registry) are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. There also exist lower-tier registries which serve a particular geographic area. + +Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). The RIR model includes a boolean flag which indicates whether the RIR allocates only private IP space. + +For example, suppose your organization has been allocated 104.131.0.0/16 by ARIN. It also makes use of RFC 1918 addressing internally. You would first create RIRs named ARIN and RFC 1918, then create an aggregate for each of these top-level prefixes, assigning it to its respective RIR. diff --git a/docs/models/ipam/role.md b/docs/models/ipam/role.md new file mode 100644 index 000000000..8623e5282 --- /dev/null +++ b/docs/models/ipam/role.md @@ -0,0 +1,3 @@ +# Prefix/VLAN Roles + +A role indicates the function of a prefix or VLAN. For example, you might define Data, Voice, and Security roles. Generally, a prefix will be assigned the same functional role as the VLAN to which it is assigned (if any). diff --git a/docs/models/ipam/service.md b/docs/models/ipam/service.md new file mode 100644 index 000000000..057544a91 --- /dev/null +++ b/docs/models/ipam/service.md @@ -0,0 +1,5 @@ +# Services + +A service represents a layer four TCP or UDP service available on a device or virtual machine. For example, you might want to document that an HTTP service is running on a device. Each service includes a name, protocol, and port number; for example, "SSH (TCP/22)" or "DNS (UDP/53)." + +A service may optionally be bound to one or more specific IP addresses belonging to its parent device or VM. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.) diff --git a/docs/models/ipam/vlan.md b/docs/models/ipam/vlan.md new file mode 100644 index 000000000..48f24006c --- /dev/null +++ b/docs/models/ipam/vlan.md @@ -0,0 +1,11 @@ +# VLANs + +A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site and/or VLAN group. + +Each VLAN must be assigned one of the following operational statuses: + +* Active +* Reserved +* Deprecated + +Each VLAN may also be assigned a functional role. Prefixes and VLANs share the same set of customizable roles. diff --git a/docs/models/ipam/vlangroup.md b/docs/models/ipam/vlangroup.md new file mode 100644 index 000000000..1fa31c522 --- /dev/null +++ b/docs/models/ipam/vlangroup.md @@ -0,0 +1,3 @@ +# VLAN Groups + +VLAN groups can be used to organize VLANs within NetBox. Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group. diff --git a/docs/models/ipam/vrf.md b/docs/models/ipam/vrf.md new file mode 100644 index 000000000..c3d3390e4 --- /dev/null +++ b/docs/models/ipam/vrf.md @@ -0,0 +1,12 @@ +# Virtual Routing and Forwarding (VRF) + +A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space). + +Each VRF is assigned a unique name and an optional route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced. + +Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any IP prefix or address not assigned to a VRF is said to belong to the "global" table. + +By default, NetBox will allow duplicate prefixes to be assigned to a VRF. This behavior can be disabled by setting the "enforce unique" flag on the VRF model. + +!!! note + Enforcement of unique IP space can be toggled for global table (non-VRF prefixes) using the `ENFORCE_GLOBAL_UNIQUE` configuration setting. diff --git a/docs/models/secrets/secret.md b/docs/models/secrets/secret.md new file mode 100644 index 000000000..4c3482624 --- /dev/null +++ b/docs/models/secrets/secret.md @@ -0,0 +1,5 @@ +# Secrets + +A secret represents a single credential or other sensitive string of characters which must be stored securely. Each secret is assigned to a device within NetBox. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext. + +Each secret can also store an optional name parameter, which is not encrypted. This may be useful for storing user names. diff --git a/docs/models/secrets/secretrole.md b/docs/models/secrets/secretrole.md new file mode 100644 index 000000000..8997ed52a --- /dev/null +++ b/docs/models/secrets/secretrole.md @@ -0,0 +1,11 @@ +# Secret Roles + +Each secret is assigned a functional role which indicates what it is used for. Secret roles are customizable. Typical roles might include: + +* Login credentials +* SNMP community strings +* RADIUS/TACACS+ keys +* IKE key strings +* Routing protocol shared secrets + +Roles are also used to control access to secrets. Each role is assigned an arbitrary number of groups and/or users. Only the users associated with a role have permission to decrypt the secrets assigned to that role. (A superuser has permission to decrypt all secrets, provided they have an active user key.) diff --git a/docs/models/secrets/userkey.md b/docs/models/secrets/userkey.md new file mode 100644 index 000000000..fd66c7bdb --- /dev/null +++ b/docs/models/secrets/userkey.md @@ -0,0 +1,35 @@ +# User Keys + +Each user within NetBox can associate his or her account with an RSA public key. If activated by an administrator, this user key will contain a unique, encrypted copy of the AES master key needed to retrieve secret data. + +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. + +To create a user key, you can either generate a new RSA key pair, or upload the public key belonging to a pair you already have. If generating a new key pair, **you must save the private key** locally before saving your new user key. Once your user key has been created, its public key will be displayed under your profile. + +When the first user key is created in NetBox, a random master encryption key is generated automatically. This key is then encrypted using the public key provided and stored as part of your user key. **The master key cannot be recovered** without your private key. + +Once a user key has been assigned an encrypted copy of the master key, it is considered activated and can now be used to encrypt and decrypt secrets. + +## Creating Additional User Keys + +Any user can create his or her user key by generating or uploading a public RSA key. However, a user key cannot be used to encrypt or decrypt secrets until it has been activated with an encrypted copy of the master key. + +Only an administrator with an active user key can activate other user keys. To do so, access the NetBox admin UI and navigate to Secrets > User Keys. Select the user key(s) to be activated, and select "activate selected user keys" from the actions dropdown. You will need to provide your private key in order to decrypt the master key. A copy of the master key is then encrypted using the public key associated with the user key being activated. diff --git a/docs/models/tenancy/tenant.md b/docs/models/tenancy/tenant.md new file mode 100644 index 000000000..f7cf68ab8 --- /dev/null +++ b/docs/models/tenancy/tenant.md @@ -0,0 +1,16 @@ +# Tenants + +A tenant represents a discrete entity for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. The following objects can be assigned to tenants: + +* Sites +* Racks +* Rack reservations +* Devices +* VRFs +* Prefixes +* IP addresses +* VLANs +* Circuits +* Virtual machines + +Tenant assignment is used to signify ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate. diff --git a/docs/models/tenancy/tenantgroup.md b/docs/models/tenancy/tenantgroup.md new file mode 100644 index 000000000..48d9f4b6e --- /dev/null +++ b/docs/models/tenancy/tenantgroup.md @@ -0,0 +1,3 @@ +# Tenant Groups + +Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional. diff --git a/docs/models/virtualization/cluster.md b/docs/models/virtualization/cluster.md new file mode 100644 index 000000000..6d8ce4214 --- /dev/null +++ b/docs/models/virtualization/cluster.md @@ -0,0 +1,5 @@ +# Clusters + +A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type, and may optionally be assigned to a group and/or site. + +Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular VM may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device. diff --git a/docs/models/virtualization/clustergroup.md b/docs/models/virtualization/clustergroup.md new file mode 100644 index 000000000..9e1e17315 --- /dev/null +++ b/docs/models/virtualization/clustergroup.md @@ -0,0 +1,3 @@ +# Cluster Groups + +Cluster groups may be created for the purpose of organizing clusters. The assignment of clusters to groups is optional. diff --git a/docs/models/virtualization/clustertype.md b/docs/models/virtualization/clustertype.md new file mode 100644 index 000000000..cee557df3 --- /dev/null +++ b/docs/models/virtualization/clustertype.md @@ -0,0 +1,3 @@ +# Cluster Types + +A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider. diff --git a/docs/models/virtualization/virtualmachine.md b/docs/models/virtualization/virtualmachine.md new file mode 100644 index 000000000..5a82f8267 --- /dev/null +++ b/docs/models/virtualization/virtualmachine.md @@ -0,0 +1,11 @@ +# Virtual Machines + +A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be associated with exactly one cluster. + +Like devices, each VM can be assigned a platform and have interfaces created on it. VM interfaces behave similarly to device interfaces, and can be assigned IP addresses, VLANs, and services. However, given their virtual nature, they cannot be connected to other interfaces. Unlike physical devices, VMs cannot be assigned console or power ports, device bays, or inventory items. + +The following resources can be defined for each VM: + +* vCPU count +* Memory (MB) +* Disk space (GB) diff --git a/docs/release-notes/version-1.0.md b/docs/release-notes/version-1.0.md deleted file mode 100644 index ae1a35d07..000000000 --- a/docs/release-notes/version-1.0.md +++ /dev/null @@ -1,22 +0,0 @@ -# v1.0.7-r1 (2016-07-05) - -* [#199](https://github.com/netbox-community/netbox/issues/199) - Correct IP address validation - ---- - -# v1.0.7 (2016-06-30) - -**Note:** If upgrading from a previous release, be sure to run ./upgrade.sh after downloading the new code. -* [#135](https://github.com/netbox-community/netbox/issues/135) - Fixed display of navigation menu on mobile screens -* [#141](https://github.com/netbox-community/netbox/issues/141) - Fixed rendering of "getting started" guide -* Modified upgrade.sh to use sudo for pip installations -* [#109](https://github.com/netbox-community/netbox/issues/109) - Hide the navigation menu from anonymous users if login is required -* [#143](https://github.com/netbox-community/netbox/issues/143) - Add help_text to Device.position -* [#136](https://github.com/netbox-community/netbox/issues/136) - Prefixes which have host bits set will trigger an error instead of being silently corrected -* [#140](https://github.com/netbox-community/netbox/issues/140) - Improved support for Unicode in object names - ---- - -# v1.0.0 (2016-06-27) - -NetBox was originally developed internally at DigitalOcean by the network development team. This release marks the debut of NetBox as an open source project. diff --git a/docs/release-notes/version-1.1.md b/docs/release-notes/version-1.1.md deleted file mode 100644 index e7c67c41e..000000000 --- a/docs/release-notes/version-1.1.md +++ /dev/null @@ -1,15 +0,0 @@ -# v1.1.0 (2016-07-07) - -## New Features - -* [#107](https://github.com/netbox-community/netbox/pull/107) - Docker support -* [#91](https://github.com/netbox-community/netbox/issues/91) - Support for subdevices within a device -* [#170](https://github.com/netbox-community/netbox/pull/170) - Added MAC address field to interfaces - -## Bug Fixes - -* [#169](https://github.com/netbox-community/netbox/issues/169) - Fix rendering of cancellation URL when editing objects -* [#183](https://github.com/netbox-community/netbox/issues/183) - Ignore vi swap files -* [#209](https://github.com/netbox-community/netbox/issues/209) - Corrected error when not confirming component template deletions -* [#214](https://github.com/netbox-community/netbox/issues/214) - Fixed redundant message on bulk interface creation -* [#68](https://github.com/netbox-community/netbox/issues/68) - Improved permissions-related error reporting for secrets diff --git a/docs/release-notes/version-1.2.md b/docs/release-notes/version-1.2.md deleted file mode 100644 index a593e774a..000000000 --- a/docs/release-notes/version-1.2.md +++ /dev/null @@ -1,48 +0,0 @@ -# v1.2.2 (2016-07-14) - -## Improvements - -* [#174](https://github.com/netbox-community/netbox/issues/174) - Added search and site filter to provider list -* [#270](https://github.com/netbox-community/netbox/issues/270) - Added the ability to filter devices by rack group - -## Bug Fixes - -* [#115](https://github.com/netbox-community/netbox/issues/115) - Fix deprecated django.core.context_processors reference -* [#268](https://github.com/netbox-community/netbox/issues/268) - Added support for entire 32-bit ASN space -* [#282](https://github.com/netbox-community/netbox/issues/282) - De-select "all" checkbox if one or more objects are deselected -* [#290](https://github.com/netbox-community/netbox/issues/290) - Always display management interfaces for a device type (even if `is_network_device` is not set) - ---- - -# v1.2.1 (2016-07-13) - -**Note:** This release introduces a new dependency ([natsort](https://pypi.python.org/pypi/natsort)). Be sure to run `upgrade.sh` if upgrading from a previous release. - -## Improvements - -* [#285](https://github.com/netbox-community/netbox/issues/285) - Added the ability to prefer IPv4 over IPv6 for primary device IPs - -## Bug Fixes - -* [#243](https://github.com/netbox-community/netbox/issues/243) - Improved ordering of device object lists -* [#271](https://github.com/netbox-community/netbox/issues/271) - Fixed primary_ip bug in secrets API -* [#274](https://github.com/netbox-community/netbox/issues/274) - Fixed primary_ip bug in DCIM admin UI -* [#275](https://github.com/netbox-community/netbox/issues/275) - Fixed bug preventing the expansion of an existing aggregate - ---- - -# v1.2.0 (2016-07-12) - -## New Features - -* [#73](https://github.com/netbox-community/netbox/issues/73) - Added optional persistent banner -* [#93](https://github.com/netbox-community/netbox/issues/73) - Ability to set both IPv4 and IPv6 primary IPs for devices -* [#203](https://github.com/netbox-community/netbox/issues/203) - Introduced support for LDAP - -## Bug Fixes - -* [#162](https://github.com/netbox-community/netbox/issues/228) - Fixed support for Unicode characters in rack/device/VLAN names -* [#228](https://github.com/netbox-community/netbox/issues/228) - Corrected conditional inclusion of device bay templates -* [#246](https://github.com/netbox-community/netbox/issues/246) - Corrected Docker build instructions -* [#260](https://github.com/netbox-community/netbox/issues/260) - Fixed error on admin UI device type list -* Miscellaneous layout improvements for mobile devices diff --git a/docs/release-notes/version-1.3.md b/docs/release-notes/version-1.3.md deleted file mode 100644 index 4974aa732..000000000 --- a/docs/release-notes/version-1.3.md +++ /dev/null @@ -1,54 +0,0 @@ -# v1.3.2 (2016-07-26) - -## Improvements - -* [#292](https://github.com/netbox-community/netbox/issues/292) - Added part_number field to DeviceType -* [#363](https://github.com/netbox-community/netbox/issues/363) - Added a description field to the VLAN model -* [#374](https://github.com/netbox-community/netbox/issues/374) - Increased VLAN name length to 64 characters -* Enabled bulk deletion of interfaces from devices - -## Bug Fixes - -* [#359](https://github.com/netbox-community/netbox/issues/359) - Corrected the DCIM API endpoint for finding related connections -* [#370](https://github.com/netbox-community/netbox/issues/370) - Notify user when secret decryption fails -* [#381](https://github.com/netbox-community/netbox/issues/381) - Fix 'u_consumed' error on rack import -* [#384](https://github.com/netbox-community/netbox/issues/384) - Fixed description field's maximum length on IPAM bulk edit forms -* [#385](https://github.com/netbox-community/netbox/issues/385) - Fixed error when deleting a user with one or more associated UserActions - ---- - -# v1.3.1 (2016-07-21) - -## Improvements - -* [#258](https://github.com/netbox-community/netbox/issues/258) - Add an API endpoint to list interface connections -* [#303](https://github.com/netbox-community/netbox/issues/303) - Improved numeric ordering of sites, racks, and devices -* [#304](https://github.com/netbox-community/netbox/issues/304) - Display utilization percentage on rack list -* [#327](https://github.com/netbox-community/netbox/issues/327) - Disable rack assignment for installed child devices - -## Bug Fixes - -* [#331](https://github.com/netbox-community/netbox/issues/331) - Add group field to VLAN bulk edit form -* Miscellaneous improvements to Unicode handling - ---- - -# v1.3.0 (2016-07-18) - -## New Features - -* [#42](https://github.com/netbox-community/netbox/issues/42) - Allow assignment of VLAN on prefix import -* [#43](https://github.com/netbox-community/netbox/issues/43) - Toggling of IP space uniqueness within a VRF -* [#111](https://github.com/netbox-community/netbox/issues/111) - Introduces VLAN groups -* [#227](https://github.com/netbox-community/netbox/issues/227) - Support for bulk import of child devices - -## Bug Fixes - -* [#301](https://github.com/netbox-community/netbox/issues/301) - Prevent deletion of DeviceBay when installed device is deleted -* [#306](https://github.com/netbox-community/netbox/issues/306) - Fixed device import to allow an unspecified rack face -* [#307](https://github.com/netbox-community/netbox/issues/307) - Catch `RelatedObjectDoesNotExist` when an invalid device type is defined during device import -* [#308](https://github.com/netbox-community/netbox/issues/308) - Update rack assignment for all child devices when moving a parent device -* [#311](https://github.com/netbox-community/netbox/issues/311) - Fix assignment of primary_ip on IP address import -* [#317](https://github.com/netbox-community/netbox/issues/317) - Rack elevation display fix for device types greater than 42U in height -* [#320](https://github.com/netbox-community/netbox/issues/320) - Disallow import of prefixes with host masks -* [#322](https://github.com/netbox-community/netbox/issues/320) - Corrected VLAN import behavior diff --git a/docs/release-notes/version-1.4.md b/docs/release-notes/version-1.4.md deleted file mode 100644 index 0408e7dbe..000000000 --- a/docs/release-notes/version-1.4.md +++ /dev/null @@ -1,54 +0,0 @@ -# v1.4.2 (2016-08-06) - -## Improvements - -* [#167](https://github.com/netbox-community/netbox/issues/167) - Added new interface form factors -* [#253](https://github.com/netbox-community/netbox/issues/253) - Added new interface form factors -* [#434](https://github.com/netbox-community/netbox/issues/434) - Restored admin UI access to user action history (however bulk deletion is disabled) -* [#435](https://github.com/netbox-community/netbox/issues/435) - Added an "add prefix" button to the VLAN view - -## Bug Fixes - -* [#425](https://github.com/netbox-community/netbox/issues/425) - Ignore leading and trailing periods when generating a slug -* [#427](https://github.com/netbox-community/netbox/issues/427) - Prevent error when duplicate IPs are present in a prefix's IP list -* [#429](https://github.com/netbox-community/netbox/issues/429) - Correct redirection of user when adding a secret to a device - ---- - -# v1.4.1 (2016-08-03) - -## Improvements - -* [#289](https://github.com/netbox-community/netbox/issues/289) - Annotate available ranges in prefix IP list -* [#412](https://github.com/netbox-community/netbox/issues/412) - Tenant group assignment is no longer mandatory -* [#422](https://github.com/netbox-community/netbox/issues/422) - CSV import now supports double-quoting values which contain commas - -## Bug Fixes - -* [#395](https://github.com/netbox-community/netbox/issues/395) - Show child prefixes from all VRFs if the parent belongs to the global table -* [#406](https://github.com/netbox-community/netbox/issues/406) - Fixed circuit list rendring when filtering on port speed or commit rate -* [#409](https://github.com/netbox-community/netbox/issues/409) - Filter IPs and prefixes by tenant slug rather than by its PK -* [#411](https://github.com/netbox-community/netbox/issues/411) - Corrected title of secret roles view -* [#419](https://github.com/netbox-community/netbox/issues/419) - Fixed a potential database performance issue when gathering tenant statistics - ---- - -# v1.4.0 (2016-08-01) - -## New Features - -### Multitenancy ([#16](https://github.com/netbox-community/netbox/issues/16)) - -NetBox now supports tenants and tenant groups. Sites, racks, devices, VRFs, prefixes, IP addresses, VLANs, and circuits can be assigned to tenants to track the allocation of these resources among customers or internal departments. If a prefix or IP address does not have a tenant assigned, it will fall back to the tenant assigned to its parent VRF (where applicable). - -## Improvements - -* [#176](https://github.com/netbox-community/netbox/issues/176) - Introduced seed data for new installs -* [#358](https://github.com/netbox-community/netbox/issues/358) - Improved search for all objects -* [#394](https://github.com/netbox-community/netbox/issues/394) - Improved VRF selection during bulk editing of prefixes and IP addresses -* Miscellaneous cosmetic improvements to the UI - -## Bug Fixes - -* [#392](https://github.com/netbox-community/netbox/issues/392) - Don't include child devices in non-racked devices table -* [#397](https://github.com/netbox-community/netbox/issues/397) - Only include child IPs which belong to the same VRF as the parent prefix diff --git a/docs/release-notes/version-1.5.md b/docs/release-notes/version-1.5.md deleted file mode 100644 index 77fbee861..000000000 --- a/docs/release-notes/version-1.5.md +++ /dev/null @@ -1,50 +0,0 @@ -# v1.5.2 (2016-08-16) - -## Bug Fixes - -* [#460](https://github.com/netbox-community/netbox/issues/460) - Corrected ordering of IP addresses with differing prefix lengths -* [#463](https://github.com/netbox-community/netbox/issues/463) - Prevent pre-population of livesearch field with '---------' -* [#467](https://github.com/netbox-community/netbox/issues/467) - Include prefixes and IPs which inherit tenancy from their VRF in tenant stats -* [#468](https://github.com/netbox-community/netbox/issues/468) - Don't allow connected interfaces to be changed to the "virtual" form factor -* [#469](https://github.com/netbox-community/netbox/issues/469) - Added missing import buttons to list views -* [#472](https://github.com/netbox-community/netbox/issues/472) - Hide the connection button for interfaces which have a circuit terminated to them - ---- - -# v1.5.1 (2016-08-11) - -## Improvements - -* [#421](https://github.com/netbox-community/netbox/issues/421) - Added an asset tag field to devices -* [#456](https://github.com/netbox-community/netbox/issues/456) - Added IP search box to home page -* Colorized rack and device roles - -## Bug Fixes - -* [#454](https://github.com/netbox-community/netbox/issues/454) - Corrected error on rack export -* [#457](https://github.com/netbox-community/netbox/issues/457) - Added role field to rack edit form - ---- - -# v1.5.0 (2016-08-10) - -## New Features - -### Rack Enhancements ([#180](https://github.com/netbox-community/netbox/issues/180), [#241](https://github.com/netbox-community/netbox/issues/241)) - -Like devices, racks can now be assigned to functional roles. This allows users to group racks by designated function as well as by physical location (rack groups). Additionally, rack can now have a defined rail-to-rail width (19 or 23 inches) and a type (two-post-rack, cabinet, etc.). - -## Improvements - -* [#149](https://github.com/netbox-community/netbox/issues/149) - Added discrete upstream speed field for circuits -* [#157](https://github.com/netbox-community/netbox/issues/157) - Added manufacturer field for device modules -* We have a logo! -* Upgraded to Django 1.10 - -## Bug Fixes - -* [#433](https://github.com/netbox-community/netbox/issues/433) - Corrected form validation when editing child devices -* [#442](https://github.com/netbox-community/netbox/issues/442) - Corrected child device import instructions -* [#443](https://github.com/netbox-community/netbox/issues/443) - Correctly display and initialize VRF for creation of new IP addresses -* [#444](https://github.com/netbox-community/netbox/issues/444) - Corrected prefix model validation -* [#445](https://github.com/netbox-community/netbox/issues/445) - Limit rack height to between 1U and 100U (inclusive) diff --git a/docs/release-notes/version-1.6.md b/docs/release-notes/version-1.6.md deleted file mode 100644 index 9c3391735..000000000 --- a/docs/release-notes/version-1.6.md +++ /dev/null @@ -1,85 +0,0 @@ -# v1.6.3 (2016-10-19) - -## Improvements - -* [#353](https://github.com/netbox-community/netbox/issues/353) - Bulk editing of device and device type interfaces -* [#527](https://github.com/netbox-community/netbox/issues/527) - Support for nullification of fields when bulk editing -* [#592](https://github.com/netbox-community/netbox/issues/592) - Allow space-delimited lists of ALLOWED_HOSTS in Docker -* [#608](https://github.com/netbox-community/netbox/issues/608) - Added "select all" button for device and device type components - -## Bug Fixes - -* [#602](https://github.com/netbox-community/netbox/issues/602) - Correct display of custom integer fields with value of 0 or 1 -* [#604](https://github.com/netbox-community/netbox/issues/604) - Correct display of unnamed devices in form selection fields -* [#611](https://github.com/netbox-community/netbox/issues/611) - Power/console/interface connection import: status field should be case-insensitive -* [#615](https://github.com/netbox-community/netbox/issues/615) - Account for BASE_PATH in static URLs and during login -* [#616](https://github.com/netbox-community/netbox/issues/616) - Correct display of custom URL fields - ---- - -# v1.6.2-r1 (2016-10-04) - -## Improvements - -* [#212](https://github.com/netbox-community/netbox/issues/212) - Introduced the `BASE_PATH` configuration setting to allow running NetBox in a URL subdirectory -* [#345](https://github.com/netbox-community/netbox/issues/345) - Bulk edit: allow user to select all objects on page or all matching query -* [#475](https://github.com/netbox-community/netbox/issues/475) - Display "add" buttons at top and bottom of all device/device type panels -* [#480](https://github.com/netbox-community/netbox/issues/480) - Improved layout on mobile devices -* [#481](https://github.com/netbox-community/netbox/issues/481) - Require interface creation before trying to assign an IP to a device -* [#575](https://github.com/netbox-community/netbox/issues/575) - Allow all valid URL schemes in custom fields -* [#579](https://github.com/netbox-community/netbox/issues/579) - Add a description field to export templates - -## Bug Fixes - -* [#466](https://github.com/netbox-community/netbox/issues/466) - Validate available free space for all instances when increasing the U height of a device type -* [#571](https://github.com/netbox-community/netbox/issues/571) - Correct rack group filter on device list -* [#576](https://github.com/netbox-community/netbox/issues/576) - Delete all relevant CustomFieldValues when deleting a CustomFieldChoice -* [#581](https://github.com/netbox-community/netbox/issues/581) - Correct initialization of custom boolean and select fields -* [#591](https://github.com/netbox-community/netbox/issues/591) - Correct display of component creation buttons in device type view - ---- - -# v1.6.1-r1 (2016-09-21) - -## Improvements -* [#415](https://github.com/netbox-community/netbox/issues/415) - Add an expand/collapse toggle button to the prefix list -* [#552](https://github.com/netbox-community/netbox/issues/552) - Allow filtering on custom select fields by "none" -* [#561](https://github.com/netbox-community/netbox/issues/561) - Make custom fields accessible from within export templates - -## Bug Fixes -* [#493](https://github.com/netbox-community/netbox/issues/493) - CSV import support for UTF-8 -* [#531](https://github.com/netbox-community/netbox/issues/531) - Order prefix list by VRF assignment -* [#542](https://github.com/netbox-community/netbox/issues/542) - Add LDAP support in Docker -* [#557](https://github.com/netbox-community/netbox/issues/557) - Add 'global' choice to VRF filter for prefixes and IP addresses -* [#558](https://github.com/netbox-community/netbox/issues/558) - Update slug field when name is populated without a key press -* [#562](https://github.com/netbox-community/netbox/issues/562) - Fixed bulk interface creation -* [#564](https://github.com/netbox-community/netbox/issues/564) - Display custom fields for all applicable objects - ---- - -# v1.6.0 (2016-09-13) - -## New Features - -### Custom Fields ([#129](https://github.com/netbox-community/netbox/issues/129)) - -Users can now create custom fields to associate arbitrary data with core NetBox objects. For example, you might want to add a geolocation tag to IP prefixes, or a ticket number to each device. Text, integer, boolean, date, URL, and selection fields are supported. - -## Improvements - -* [#489](https://github.com/netbox-community/netbox/issues/489) - Docker file now builds from a `python:2.7-wheezy` base instead of `ubuntu:14.04` -* [#540](https://github.com/netbox-community/netbox/issues/540) - Add links for VLAN roles under VLAN navigation menu -* Added new interface form factors -* Added address family filters to aggregate and prefix lists - -## Bug Fixes - -* [#476](https://github.com/netbox-community/netbox/issues/476) - Corrected rack import instructions -* [#484](https://github.com/netbox-community/netbox/issues/484) - Allow bulk deletion of >1K objects -* [#486](https://github.com/netbox-community/netbox/issues/486) - Prompt for secret key only if updating a secret's value -* [#490](https://github.com/netbox-community/netbox/issues/490) - Corrected display of circuit commit rate -* [#495](https://github.com/netbox-community/netbox/issues/495) - Include tenant in prefix and IP CSV export -* [#507](https://github.com/netbox-community/netbox/issues/507) - Corrected rendering of nav menu on screens narrower than 1200px -* [#515](https://github.com/netbox-community/netbox/issues/515) - Clarified instructions for the "face" field when importing devices -* [#522](https://github.com/netbox-community/netbox/issues/522) - Remove obsolete check for staff status when bulk deleting objects -* [#544](https://github.com/netbox-community/netbox/issues/544) - Strip CRLF-style line terminators from rendered export templates diff --git a/docs/release-notes/version-1.7.md b/docs/release-notes/version-1.7.md deleted file mode 100644 index 691cb8c92..000000000 --- a/docs/release-notes/version-1.7.md +++ /dev/null @@ -1,75 +0,0 @@ -# v1.7.3 (2016-12-08) - -## Bug Fixes - -* [#724](https://github.com/netbox-community/netbox/issues/724) - Exempt API views from LoginRequiredMiddleware to enable basic HTTP authentication when LOGIN_REQUIRED is true -* [#729](https://github.com/netbox-community/netbox/issues/729) - Corrected cancellation links when editing secondary objects -* [#732](https://github.com/netbox-community/netbox/issues/732) - Allow custom select field values to be deselected if the field is not required -* [#733](https://github.com/netbox-community/netbox/issues/733) - Fixed MAC address filter on device list -* [#734](https://github.com/netbox-community/netbox/issues/734) - Corrected display of device type when editing a device - ---- - -# v1.7.2-r1 (2016-12-06) - -## Improvements - -* [#663](https://github.com/netbox-community/netbox/issues/663) - Added MAC address search field to device list -* [#672](https://github.com/netbox-community/netbox/issues/672) - Increased the selection of available colors for rack and device roles -* [#695](https://github.com/netbox-community/netbox/issues/695) - Added is_private field to RIR - -## Bug Fixes - -* [#677](https://github.com/netbox-community/netbox/issues/677) - Fix setuptools installation error on Debian 8.6 -* [#696](https://github.com/netbox-community/netbox/issues/696) - Corrected link to VRF in prefix and IP address breadcrumbs -* [#702](https://github.com/netbox-community/netbox/issues/702) - Improved Unicode support for custom fields -* [#712](https://github.com/netbox-community/netbox/issues/712) - Corrected export of tenants which are not assigned to a group -* [#713](https://github.com/netbox-community/netbox/issues/713) - Include a label for the comments field when editing circuits, providers, or racks in bulk -* [#718](https://github.com/netbox-community/netbox/issues/718) - Restore is_primary field on IP assignment form -* [#723](https://github.com/netbox-community/netbox/issues/723) - API documentation is now accessible when using BASE_PATH -* [#727](https://github.com/netbox-community/netbox/issues/727) - Corrected error in rack elevation display (v1.7.2) - ---- - -# v1.7.1 (2016-11-15) - -## Improvements - -* [#667](https://github.com/netbox-community/netbox/issues/667) - Added prefix utilization statistics to the RIR list view -* [#685](https://github.com/netbox-community/netbox/issues/685) - When assigning an IP to a device, automatically select the interface if only one exists - -## Bug Fixes - -* [#674](https://github.com/netbox-community/netbox/issues/674) - Fix assignment of status to imported IP addresses -* [#676](https://github.com/netbox-community/netbox/issues/676) - Server error when bulk editing device types -* [#678](https://github.com/netbox-community/netbox/issues/678) - Server error on device import specifying an invalid device type -* [#691](https://github.com/netbox-community/netbox/issues/691) - Allow the assignment of power ports to PDUs -* [#692](https://github.com/netbox-community/netbox/issues/692) - Form errors are not displayed on checkbox fields - ---- - -# v1.7.0 (2016-11-03) - -## New Features - -### IP address statuses ([#87](https://github.com/netbox-community/netbox/issues/87)) - -An IP address can now be designated as active, reserved, or DHCP. The DHCP status implies that the IP address is part of a DHCP pool and may or may not be assigned to a DHCP client. - -### Top-to-bottom rack numbering ([#191](https://github.com/netbox-community/netbox/issues/191)) - -Racks can now be set to have descending rack units, with U1 at the top of the rack. When adding a device to a rack with descending units, be sure to position it in the **lowest-numbered** unit which it occupies (this will be physically the topmost unit). - -## Improvements -* [#211](https://github.com/netbox-community/netbox/issues/211) - Allow device assignment and removal from IP address view -* [#630](https://github.com/netbox-community/netbox/issues/630) - Added a custom 404 page -* [#652](https://github.com/netbox-community/netbox/issues/652) - Use password input controls when editing secrets -* [#654](https://github.com/netbox-community/netbox/issues/654) - Added Cisco FlexStack and FlexStack Plus form factors -* [#661](https://github.com/netbox-community/netbox/issues/661) - Display relevant IP addressing when viewing a circuit - -## Bug Fixes -* [#632](https://github.com/netbox-community/netbox/issues/632) - Use semicolons instead of commas to separate regexes in topology maps -* [#647](https://github.com/netbox-community/netbox/issues/647) - Extend form used when assigning an IP to a device -* [#657](https://github.com/netbox-community/netbox/issues/657) - Unicode error when adding device modules -* [#660](https://github.com/netbox-community/netbox/issues/660) - Corrected calculation of utilized space in rack list -* [#664](https://github.com/netbox-community/netbox/issues/664) - Fixed bulk creation of interfaces across multiple devices diff --git a/docs/release-notes/version-1.8.md b/docs/release-notes/version-1.8.md deleted file mode 100644 index e35b8228e..000000000 --- a/docs/release-notes/version-1.8.md +++ /dev/null @@ -1,106 +0,0 @@ -# v1.8.4 (2017-02-03) - -## Improvements - -* [#856](https://github.com/netbox-community/netbox/issues/856) - Strip whitespace from fields during CSV import - -## Bug Fixes - -* [#851](https://github.com/netbox-community/netbox/issues/851) - Resolve encoding issues during import/export (Python 3) -* [#854](https://github.com/netbox-community/netbox/issues/854) - Correct processing of get_return_url() in ObjectDeleteView -* [#859](https://github.com/netbox-community/netbox/issues/859) - Fix Javascript for connection status toggle button on device view -* [#861](https://github.com/netbox-community/netbox/issues/861) - Avoid overwriting device primary IP assignment from alternate family during bulk import of IP addresses -* [#865](https://github.com/netbox-community/netbox/issues/865) - Fix server error when attempting to delete a protected object parent (Python 3) - ---- - -# v1.8.3 (2017-01-26) - -## Improvements - -* [#782](https://github.com/netbox-community/netbox/issues/782) - Allow filtering devices list by manufacturer -* [#820](https://github.com/netbox-community/netbox/issues/820) - Add VLAN column to parent prefixes table on IP address view -* [#821](https://github.com/netbox-community/netbox/issues/821) - Support for comma separation in bulk IP/interface creation -* [#827](https://github.com/netbox-community/netbox/issues/827) - **Introduced support for Python 3** -* [#836](https://github.com/netbox-community/netbox/issues/836) - Add "deprecated" status for IP addresses -* [#841](https://github.com/netbox-community/netbox/issues/841) - Merged search and filter forms on all object lists - -## Bug Fixes - -* [#816](https://github.com/netbox-community/netbox/issues/816) - Redirect back to parent prefix view after deleting child prefixes termination -* [#817](https://github.com/netbox-community/netbox/issues/817) - Update last_updated time of a circuit when editing a child termination -* [#830](https://github.com/netbox-community/netbox/issues/830) - Redirect user to device view after editing a device component -* [#840](https://github.com/netbox-community/netbox/issues/840) - Correct API path resolution for secrets when BASE_PATH is configured -* [#844](https://github.com/netbox-community/netbox/issues/844) - Apply order_naturally() to API interfaces list -* [#845](https://github.com/netbox-community/netbox/issues/845) - Fix missing edit/delete buttons on object tables for non-superusers - ---- - -# v1.8.2 (2017-01-18) - -## Improvements - -* [#284](https://github.com/netbox-community/netbox/issues/284) - Enabled toggling of interface display order per device type -* [#760](https://github.com/netbox-community/netbox/issues/760) - Redirect user back to device view after deleting an assigned IP address -* [#783](https://github.com/netbox-community/netbox/issues/783) - Add a description field to the Circuit model -* [#797](https://github.com/netbox-community/netbox/issues/797) - Add description column to VLANs table -* [#803](https://github.com/netbox-community/netbox/issues/803) - Clarify that no child objects are deleted when deleting a prefix -* [#805](https://github.com/netbox-community/netbox/issues/805) - Linkify site column in device table - -## Bug Fixes - -* [#776](https://github.com/netbox-community/netbox/issues/776) - Prevent circuits from appearing twice while searching -* [#778](https://github.com/netbox-community/netbox/issues/778) - Corrected an issue preventing multiple interfaces with the same position ID from appearing in a device's interface list -* [#785](https://github.com/netbox-community/netbox/issues/785) - Trigger validation error when importing a prefix assigned to a nonexistent VLAN -* [#802](https://github.com/netbox-community/netbox/issues/802) - Fixed enforcement of ENFORCE_GLOBAL_UNIQUE for prefixes -* [#807](https://github.com/netbox-community/netbox/issues/807) - Redirect user back to form when adding IP addresses in bulk and "create and add another" is clicked -* [#810](https://github.com/netbox-community/netbox/issues/810) - Suppress unique IP validation on invalid IP addresses and prefixes - ---- - -# v1.8.1 (2017-01-04) - -## Improvements - -* [#771](https://github.com/netbox-community/netbox/issues/771) - Don't automatically redirect user when only one object is returned in a list - -## Bug Fixes - -* [#764](https://github.com/netbox-community/netbox/issues/764) - Encapsulate in double quotes values containing commas when exporting to CSV -* [#767](https://github.com/netbox-community/netbox/issues/767) - Fixes xconnect_id error when searching for circuits -* [#769](https://github.com/netbox-community/netbox/issues/769) - Show default value for boolean custom fields -* [#772](https://github.com/netbox-community/netbox/issues/772) - Fixes TypeError in API RackUnitListView when no device is excluded - ---- - -# v1.8.0 (2017-01-03) - -## New Features - -### Point-to-Point Circuits ([#49](https://github.com/netbox-community/netbox/issues/49)) - -Until now, NetBox has supported tracking only one end of a data circuit. This is fine for Internet connections where you don't care (or know) much about the provider side of the circuit, but many users need the ability to track inter-site circuits as well. This release expands circuit modeling so that each circuit can have an A and/or Z side. Each endpoint must be terminated to a site, and may optionally be terminated to a specific device and interface within that site. - -### L4 Services ([#539](https://github.com/netbox-community/netbox/issues/539)) - -Our first major community contribution introduces the ability to track discrete TCP and UDP services associated with a device (for example, SSH or HTTP). Each service can optionally be assigned to one or more specific IP addresses belonging to the device. Thanks to [@if-fi](https://github.com/if-fi) for the addition! - -## Improvements - -* [#122](https://github.com/netbox-community/netbox/issues/122) - Added comments field to device types -* [#181](https://github.com/netbox-community/netbox/issues/181) - Implemented support for bulk IP address creation -* [#613](https://github.com/netbox-community/netbox/issues/613) - Added prefixes column to VLAN list; added VLAN column to prefix list -* [#716](https://github.com/netbox-community/netbox/issues/716) - Add ASN field to site bulk edit form -* [#722](https://github.com/netbox-community/netbox/issues/722) - Enabled custom fields for device types -* [#743](https://github.com/netbox-community/netbox/issues/743) - Enabled bulk creation of all device components -* [#756](https://github.com/netbox-community/netbox/issues/756) - Added contact details to site model - -## Bug Fixes - -* [#563](https://github.com/netbox-community/netbox/issues/563) - Allow a device to be flipped from one rack face to the other without moving it -* [#658](https://github.com/netbox-community/netbox/issues/658) - Enabled conditional treatment of network/broadcast IPs for a prefix by defining it as a pool -* [#741](https://github.com/netbox-community/netbox/issues/741) - Hide "select all" button for users without edit permissions -* [#744](https://github.com/netbox-community/netbox/issues/744) - Fixed export of sites without an AS number -* [#747](https://github.com/netbox-community/netbox/issues/747) - Fixed natural_order_by integer cast error on large numbers -* [#751](https://github.com/netbox-community/netbox/issues/751) - Fixed python-cryptography installation issue on Debian -* [#763](https://github.com/netbox-community/netbox/issues/763) - Added missing fields to CSV exports for racks and prefixes diff --git a/docs/release-notes/version-1.9.md b/docs/release-notes/version-1.9.md deleted file mode 100644 index bcde1032c..000000000 --- a/docs/release-notes/version-1.9.md +++ /dev/null @@ -1,136 +0,0 @@ -# v1.9.6 (2017-04-21) - -## Improvements - -* [#878](https://github.com/netbox-community/netbox/issues/878) - Merged IP addresses with interfaces list on device view -* [#1001](https://github.com/netbox-community/netbox/issues/1001) - Interface assignment can be modified when editing an IP address -* [#1084](https://github.com/netbox-community/netbox/issues/1084) - Include custom fields when creating IP addresses in bulk - -## Bug Fixes - -* [#1057](https://github.com/netbox-community/netbox/issues/1057) - Corrected VLAN validation during prefix import -* [#1061](https://github.com/netbox-community/netbox/issues/1061) - Fixed potential for script injection via create/edit/delete messages -* [#1070](https://github.com/netbox-community/netbox/issues/1070) - Corrected installation instructions for Python3 on CentOS/RHEL -* [#1071](https://github.com/netbox-community/netbox/issues/1071) - Protect assigned circuit termination when an interface is deleted -* [#1072](https://github.com/netbox-community/netbox/issues/1072) - Order LAG interfaces naturally on bulk interface edit form -* [#1074](https://github.com/netbox-community/netbox/issues/1074) - Require ncclient 0.5.3 (Python 3 fix) -* [#1090](https://github.com/netbox-community/netbox/issues/1090) - Improved installation documentation for Python 3 -* [#1092](https://github.com/netbox-community/netbox/issues/1092) - Increase randomness in SECRET_KEY generation tool - ---- - -# v1.9.5 (2017-04-06) - -## Improvements - -* [#1052](https://github.com/netbox-community/netbox/issues/1052) - Added rack reservation list and bulk delete views - -## Bug Fixes - -* [#1038](https://github.com/netbox-community/netbox/issues/1038) - Suppress upgrading to Django 1.11 (will be supported in v2.0) -* [#1037](https://github.com/netbox-community/netbox/issues/1037) - Fixed error on VLAN import with duplicate VLAN group names -* [#1047](https://github.com/netbox-community/netbox/issues/1047) - Correct ordering of numbered subinterfaces -* [#1051](https://github.com/netbox-community/netbox/issues/1051) - Upgraded django-rest-swagger - ---- - -# v1.9.4-r1 (2017-04-04) - -## Improvements - -* [#362](https://github.com/netbox-community/netbox/issues/362) - Added per_page query parameter to control pagination page length - -## Bug Fixes - -* [#991](https://github.com/netbox-community/netbox/issues/991) - Correct server error on "create and connect another" interface connection -* [#1022](https://github.com/netbox-community/netbox/issues/1022) - Record user actions when creating IP addresses in bulk -* [#1027](https://github.com/netbox-community/netbox/issues/1027) - Fixed nav menu highlighting when BASE_PATH is set -* [#1034](https://github.com/netbox-community/netbox/issues/1034) - Added migration missing from v1.9.4 release - ---- - -# v1.9.3 (2017-03-23) - -## Improvements - -* [#972](https://github.com/netbox-community/netbox/issues/972) - Add ability to filter connections list by device name -* [#974](https://github.com/netbox-community/netbox/issues/974) - Added MAC address filter to API interfaces list -* [#978](https://github.com/netbox-community/netbox/issues/978) - Allow filtering device types by function and subdevice role -* [#981](https://github.com/netbox-community/netbox/issues/981) - Allow filtering primary objects by a given set of IDs -* [#983](https://github.com/netbox-community/netbox/issues/983) - Include peer device names when listing circuits in device view - -## Bug Fixes - -* [#967](https://github.com/netbox-community/netbox/issues/967) - Fix error when assigning a new interface to a LAG - ---- - -# v1.9.2 (2017-03-14) - -## Bug Fixes - -* [#950](https://github.com/netbox-community/netbox/issues/950) - Fix site_id error on child device import -* [#956](https://github.com/netbox-community/netbox/issues/956) - Correct bug affecting unnamed rackless devices -* [#957](https://github.com/netbox-community/netbox/issues/957) - Correct device site filter count to include unracked devices -* [#963](https://github.com/netbox-community/netbox/issues/963) - Fix bug in IPv6 address range expansion -* [#964](https://github.com/netbox-community/netbox/issues/964) - Fix bug when bulk editing/deleting filtered set of objects - ---- - -# v1.9.1 (2017-03-08) - -## Improvements - -* [#945](https://github.com/netbox-community/netbox/issues/945) - Display the current user in the navigation menu -* [#946](https://github.com/netbox-community/netbox/issues/946) - Disregard mask length when filtering IP addresses by a parent prefix - -## Bug Fixes - -* [#941](https://github.com/netbox-community/netbox/issues/941) - Corrected old references to rack.site on Device -* [#943](https://github.com/netbox-community/netbox/issues/943) - Child prefixes missing on Python 3 -* [#944](https://github.com/netbox-community/netbox/issues/944) - Corrected console and power connection form behavior -* [#948](https://github.com/netbox-community/netbox/issues/948) - Region name should be hyperlinked to site list - ---- - -# v1.9.0-r1 (2017-03-03) - -## New Features - -### Rack Reservations ([#36](https://github.com/netbox-community/netbox/issues/36)) - -Users can now reserve an arbitrary number of units within a rack, adding a comment noting their intentions. Reservations do not interfere with installed devices: It is possible to reserve a unit for future use even if it is currently occupied by a device. - -### Interface Groups ([#105](https://github.com/netbox-community/netbox/issues/105)) - -A new Link Aggregation Group (LAG) virtual form factor has been added. Physical interfaces can be assigned to a parent LAG interface to represent a port-channel or similar logical bundling of links. - -### Regions ([#164](https://github.com/netbox-community/netbox/issues/164)) - -A new region model has been introduced to allow for the geographic organization of sites. Regions can be nested recursively to form a hierarchy. - -### Rackless Devices ([#198](https://github.com/netbox-community/netbox/issues/198)) - -Previous releases required each device to be assigned to a particular rack within a site. This requirement has been relaxed so that devices must only be assigned to a site, and may optionally be assigned to a rack. - -### Global VLANs ([#235](https://github.com/netbox-community/netbox/issues/235)) - -Assignment of VLANs and VLAN groups to sites is now optional, allowing for the representation of a VLAN spanning multiple sites. - -## Improvements - -* [#862](https://github.com/netbox-community/netbox/issues/862) - Show both IPv6 and IPv4 primary IPs in device list -* [#894](https://github.com/netbox-community/netbox/issues/894) - Expand device name max length to 64 characters -* [#898](https://github.com/netbox-community/netbox/issues/898) - Expanded circuits list in provider view rack face -* [#901](https://github.com/netbox-community/netbox/issues/901) - Support for filtering prefixes and IP addresses by mask length - -## Bug Fixes - -* [#872](https://github.com/netbox-community/netbox/issues/872) - Fixed TypeError on bulk IP address creation (Python 3) -* [#884](https://github.com/netbox-community/netbox/issues/884) - Preserve selected rack unit when changing a device's rack face -* [#892](https://github.com/netbox-community/netbox/issues/892) - Restored missing edit/delete buttons when viewing child prefixes and IP addresses from a parent object -* [#897](https://github.com/netbox-community/netbox/issues/897) - Fixed power connections CSV export -* [#903](https://github.com/netbox-community/netbox/issues/903) - Only alert on missing critical connections if present in the parent device type -* [#935](https://github.com/netbox-community/netbox/issues/935) - Fix form validation error when connecting an interface using live search -* [#937](https://github.com/netbox-community/netbox/issues/937) - Region assignment should be optional when creating a site -* [#938](https://github.com/netbox-community/netbox/issues/938) - Provider view yields an error if one or more circuits is assigned to a tenant diff --git a/docs/release-notes/version-2.0.md b/docs/release-notes/version-2.0.md index 3cd1babf4..73754f904 100644 --- a/docs/release-notes/version-2.0.md +++ b/docs/release-notes/version-2.0.md @@ -1,6 +1,8 @@ -# v2.0.10 (2017-07-14) +# NetBox v2.0 Release Notes -## Bug Fixes +## v2.0.10 (2017-07-14) + +### Bug Fixes * [#1312](https://github.com/netbox-community/netbox/issues/1312) - Catch error when attempting to activate a user key with an invalid private key * [#1333](https://github.com/netbox-community/netbox/issues/1333) - Corrected label on is_console_server field of DeviceType bulk edit form @@ -10,24 +12,24 @@ --- -# v2.0.9 (2017-07-10) +## v2.0.9 (2017-07-10) -## Bug Fixes +### Bug Fixes * [#1319](https://github.com/netbox-community/netbox/issues/1319) - Fixed server error when attempting to create console/power connections * [#1325](https://github.com/netbox-community/netbox/issues/1325) - Retain interface attachment when editing a circuit termination --- -# v2.0.8 (2017-07-05) +## v2.0.8 (2017-07-05) -## Enhancements +### Enhancements * [#1298](https://github.com/netbox-community/netbox/issues/1298) - Calculate prefix utilization based on its status (container or non-container) * [#1303](https://github.com/netbox-community/netbox/issues/1303) - Highlight installed interface connections in green on device view * [#1315](https://github.com/netbox-community/netbox/issues/1315) - Enforce lowercase file extensions for image attachments -## Bug Fixes +### Bug Fixes * [#1279](https://github.com/netbox-community/netbox/issues/1279) - Fix primary_ip assignment during IP address import * [#1281](https://github.com/netbox-community/netbox/issues/1281) - Show LLDP neighbors tab on device view only if necessary conditions are met @@ -39,13 +41,13 @@ --- -# v2.0.7 (2017-06-15) +## v2.0.7 (2017-06-15) -## Enhancements +### Enhancements * [#626](https://github.com/netbox-community/netbox/issues/626) - Added bulk disconnect function for console/power/interface connections on device view -## Bug Fixes +### Bug Fixes * [#1238](https://github.com/netbox-community/netbox/issues/1238) - Fix error when editing an IP with a NAT assignment which has no assigned device * [#1263](https://github.com/netbox-community/netbox/issues/1263) - Differentiate add and edit permissions for objects @@ -58,9 +60,9 @@ --- -# v2.0.6 (2017-06-12) +## v2.0.6 (2017-06-12) -## Enhancements +### Enhancements * [#40](https://github.com/netbox-community/netbox/issues/40) - Added IP utilization graph to prefix list * [#704](https://github.com/netbox-community/netbox/issues/704) - Allow filtering VLANs by group when editing prefixes @@ -68,25 +70,25 @@ * [#990](https://github.com/netbox-community/netbox/issues/990) - Enable logging configuration in configuration.py * [#1180](https://github.com/netbox-community/netbox/issues/1180) - Simplified the process of finding related devices when viewing a device -## Bug Fixes +### Bug Fixes * [#1253](https://github.com/netbox-community/netbox/issues/1253) - Improved `upgrade.sh` to allow forcing Python2 --- -# v2.0.5 (2017-06-08) +## v2.0.5 (2017-06-08) -## Notes +### Notes The maximum number of objects an API consumer can request has been set to 1000 (e.g. `?limit=1000`). This limit can be modified by defining `MAX_PAGE_SIZE` in confgiuration.py. (To remove this limit, set `MAX_PAGE_SIZE=0`.) -## Enhancements +### Enhancements * [#655](https://github.com/netbox-community/netbox/issues/655) - Implemented header-based CSV import of objects * [#1190](https://github.com/netbox-community/netbox/issues/1190) - Allow partial string matching when searching on custom fields * [#1237](https://github.com/netbox-community/netbox/issues/1237) - Enabled setting limit=0 to disable pagination in API requests; added `MAX_PAGE_SIZE` configuration setting -## Bug Fixes +### Bug Fixes * [#837](https://github.com/netbox-community/netbox/issues/837) - Enforce uniqueness where applicable during bulk import of IP addresses * [#1226](https://github.com/netbox-community/netbox/issues/1226) - Improved validation for custom field values submitted via the API @@ -99,9 +101,9 @@ The maximum number of objects an API consumer can request has been set to 1000 ( --- -# v2.0.4 (2017-05-25) +## v2.0.4 (2017-05-25) -## Bug Fixes +### Bug Fixes * [#1206](https://github.com/netbox-community/netbox/issues/1206) - Fix redirection in admin UI after activating secret keys when BASE_PATH is set * [#1207](https://github.com/netbox-community/netbox/issues/1207) - Include nested LAG serializer when showing interface connections (API) @@ -115,14 +117,14 @@ The maximum number of objects an API consumer can request has been set to 1000 ( --- -# v2.0.3 (2017-05-18) +## v2.0.3 (2017-05-18) -## Enhancements +### Enhancements * [#1196](https://github.com/netbox-community/netbox/issues/1196) - Added a lag_id filter to the API interfaces view * [#1198](https://github.com/netbox-community/netbox/issues/1198) - Allow filtering unracked devices on device list -## Bug Fixes +### Bug Fixes * [#1157](https://github.com/netbox-community/netbox/issues/1157) - Hide nav menu search bar on small displays * [#1186](https://github.com/netbox-community/netbox/issues/1186) - Corrected VLAN edit form so that site assignment is not required @@ -137,9 +139,9 @@ The maximum number of objects an API consumer can request has been set to 1000 ( --- -# v2.0.2 (2017-05-15) +## v2.0.2 (2017-05-15) -## Enhancements +### Enhancements * [#1122](https://github.com/netbox-community/netbox/issues/1122) - Include NAT inside IPs in IP address list * [#1137](https://github.com/netbox-community/netbox/issues/1137) - Allow filtering devices list by rack @@ -149,7 +151,7 @@ The maximum number of objects an API consumer can request has been set to 1000 ( * [#1179](https://github.com/netbox-community/netbox/issues/1179) - Adjust topology map text color based on node background * On all object edit forms, allow filtering the tenant list by tenant group -## Bug Fixes +### Bug Fixes * [#1158](https://github.com/netbox-community/netbox/issues/1158) - Exception thrown when creating a device component with an invalid name * [#1159](https://github.com/netbox-community/netbox/issues/1159) - Only superusers can see "edit IP" buttons on the device interfaces list @@ -162,9 +164,9 @@ The maximum number of objects an API consumer can request has been set to 1000 ( --- -# v2.0.1 (2017-05-10) +## v2.0.1 (2017-05-10) -## Bug Fixes +### Bug Fixes * [#1149](https://github.com/netbox-community/netbox/issues/1149) - Port list does not populate when creating a console or power connection * [#1150](https://github.com/netbox-community/netbox/issues/1150) - Error when uploading image attachments with Unicode names under Python 2 @@ -174,27 +176,27 @@ The maximum number of objects an API consumer can request has been set to 1000 ( --- -# v2.0.0 (2017-05-09) +## v2.0.0 (2017-05-09) -## New Features +### New Features -### API 2.0 ([#113](https://github.com/netbox-community/netbox/issues/113)) +#### API 2.0 ([#113](https://github.com/netbox-community/netbox/issues/113)) The NetBox API has been completely rewritten and now features full read/write ability. -### Image Attachments ([#152](https://github.com/netbox-community/netbox/issues/152)) +#### Image Attachments ([#152](https://github.com/netbox-community/netbox/issues/152)) Users are now able to attach photos and other images to sites, racks, and devices. (Please ensure that the new `media` directory is writable by the system account NetBox runs as.) -### Global Search ([#159](https://github.com/netbox-community/netbox/issues/159)) +#### Global Search ([#159](https://github.com/netbox-community/netbox/issues/159)) NetBox now supports searching across all primary object types at once. -### Rack Elevations View ([#951](https://github.com/netbox-community/netbox/issues/951)) +#### Rack Elevations View ([#951](https://github.com/netbox-community/netbox/issues/951)) A new view has been introduced to display the elevations of multiple racks side-by-side. -## Enhancements +### Enhancements * [#154](https://github.com/netbox-community/netbox/issues/154) - Expanded device status field to include options other than active/offline * [#430](https://github.com/netbox-community/netbox/issues/430) - Include circuits when rendering topology maps @@ -202,7 +204,7 @@ A new view has been introduced to display the elevations of multiple racks side- * [#1100](https://github.com/netbox-community/netbox/issues/1100) - Add a "view all" link to completed bulk import views is_pool for prefixes) * [#1110](https://github.com/netbox-community/netbox/issues/1110) - Expand bulk edit forms to include boolean fields (e.g. toggle is_pool for prefixes) -## Bug Fixes +### Bug Fixes From v1.9.6: @@ -223,7 +225,7 @@ From beta3: * [#1126](https://github.com/netbox-community/netbox/issues/1126) - Fixed server error when editing a user key via admin UI attachment * [#1132](https://github.com/netbox-community/netbox/issues/1132) - Prompt user to unlock session key when importing secrets -## Additional Changes +### Additional Changes * The Module DCIM model has been renamed to InventoryItem to better reflect its intended function, and to make room for work on [#824](https://github.com/netbox-community/netbox/issues/824). * Redundant portions of the admin UI have been removed ([#973](https://github.com/netbox-community/netbox/issues/973)). diff --git a/docs/release-notes/version-2.1.md b/docs/release-notes/version-2.1.md index bfc4ede52..59f23c090 100644 --- a/docs/release-notes/version-2.1.md +++ b/docs/release-notes/version-2.1.md @@ -1,6 +1,8 @@ -# v2.1.6 (2017-10-11) +# NetBox v2.1 Release Notes -## Enhancements +## v2.1.6 (2017-10-11) + +### Enhancements * [#1548](https://github.com/netbox-community/netbox/issues/1548) - Automatically populate tenant assignment when adding an IP address from the prefix view * [#1561](https://github.com/netbox-community/netbox/issues/1561) - Added primary IP to the devices table in global search @@ -8,9 +10,9 @@ --- -# v2.1.5 (2017-09-25) +## v2.1.5 (2017-09-25) -## Enhancements +### Enhancements * [#1484](https://github.com/netbox-community/netbox/issues/1484) - Added individual "add VLAN" buttons on the VLAN groups list * [#1485](https://github.com/netbox-community/netbox/issues/1485) - Added `BANNER_LOGIN` configuration setting to display a banner on the login page @@ -18,7 +20,7 @@ * [#1523](https://github.com/netbox-community/netbox/issues/1523) - Improved the natural ordering of interfaces (thanks to [@tarkatronic](https://github.com/tarkatronic)) * [#1536](https://github.com/netbox-community/netbox/issues/1536) - Improved formatting of aggregate prefix statistics -## Bug Fixes +### Bug Fixes * [#1469](https://github.com/netbox-community/netbox/issues/1469) - Allow a NAT IP to be assigned as the primary IP for a device * [#1472](https://github.com/netbox-community/netbox/issues/1472) - Prevented truncation when displaying secret strings containing HTML characters @@ -30,9 +32,9 @@ --- -# v2.1.4 (2017-08-30) +## v2.1.4 (2017-08-30) -## Enhancements +### Enhancements * [#1326](https://github.com/netbox-community/netbox/issues/1326) - Added dropdown widget with common values for circuit speed fields * [#1341](https://github.com/netbox-community/netbox/issues/1341) - Added a `MEDIA_ROOT` configuration setting to specify where uploaded files are stored on disk @@ -42,7 +44,7 @@ * [#1432](https://github.com/netbox-community/netbox/issues/1432) - Added a `commit_rate` field to the circuits list search form * [#1460](https://github.com/netbox-community/netbox/issues/1460) - Hostnames with no domain are now acceptable in custom URL fields -## Bug Fixes +### Bug Fixes * [#1429](https://github.com/netbox-community/netbox/issues/1429) - Fixed uptime formatting on device status page * [#1433](https://github.com/netbox-community/netbox/issues/1433) - Fixed `devicetype_id` filter for DeviceType components @@ -51,9 +53,9 @@ --- -# v2.1.3 (2017-08-15) +## v2.1.3 (2017-08-15) -## Bug Fixes +### Bug Fixes * [#1330](https://github.com/netbox-community/netbox/issues/1330) - Raise validation error when assigning an unrelated IP as the primary IP for a device * [#1389](https://github.com/netbox-community/netbox/issues/1389) - Avoid splitting carat/prefix on prefix list @@ -66,23 +68,23 @@ --- -# v2.1.2 (2017-08-04) +## v2.1.2 (2017-08-04) -## Enhancements +### Enhancements * [#992](https://github.com/netbox-community/netbox/issues/992) - Allow the creation of multiple services per device with the same protocol and port * Tweaked navigation menu styling -## Bug Fixes +### Bug Fixes * [#1388](https://github.com/netbox-community/netbox/issues/1388) - Fixed server error when searching globally for IPs/prefixes (rolled back #1379) * [#1390](https://github.com/netbox-community/netbox/issues/1390) - Fixed IndexError when viewing available IPs within large IPv6 prefixes --- -# v2.1.1 (2017-08-02) +## v2.1.1 (2017-08-02) -## Enhancements +### Enhancements * [#893](https://github.com/netbox-community/netbox/issues/893) - Allow filtering by null values for NullCharacterFields (e.g. return only unnamed devices) * [#1368](https://github.com/netbox-community/netbox/issues/1368) - Render reservations in rack elevations view @@ -90,18 +92,18 @@ * [#1375](https://github.com/netbox-community/netbox/issues/1375) - Renamed `NETBOX_USERNAME` and `NETBOX_PASSWORD` configuration parameters to `NAPALM_USERNAME` and `NAPALM_PASSWORD` * [#1379](https://github.com/netbox-community/netbox/issues/1379) - Allow searching devices by interface MAC address in global search -## Bug Fixes +### Bug Fixes * [#461](https://github.com/netbox-community/netbox/issues/461) - Display a validation error when attempting to assigning a new child device to a rack face/position * [#1385](https://github.com/netbox-community/netbox/issues/1385) - Connected device API endpoint no longer requires authentication if `LOGIN_REQUIRED` is False --- -# v2.1.0 (2017-07-25) +## v2.1.0 (2017-07-25) -## New Features +### New Features -### IP Address Roles ([#819](https://github.com/netbox-community/netbox/issues/819)) +#### IP Address Roles ([#819](https://github.com/netbox-community/netbox/issues/819)) The IP address model now supports the assignment of a functional role to help identify special-purpose IPs. These include: @@ -113,15 +115,15 @@ The IP address model now supports the assignment of a functional role to help id * HSRP * GLBP -### Automatic Provisioning of Next Available IP ([#1246](https://github.com/netbox-community/netbox/issues/1246)) +#### Automatic Provisioning of Next Available IP ([#1246](https://github.com/netbox-community/netbox/issues/1246)) A new API endpoint has been added at `/api/ipam/prefixes//available-ips/`. A GET request to this endpoint will return a list of available IP addresses within the prefix (up to the pagination limit). A POST request will automatically create and return the next available IP address. -### NAPALM Integration ([#1348](https://github.com/netbox-community/netbox/issues/1348)) +#### NAPALM Integration ([#1348](https://github.com/netbox-community/netbox/issues/1348)) The [NAPALM automation](https://napalm-automation.net/) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](http://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py. -## Enhancements +### Enhancements * [#838](https://github.com/netbox-community/netbox/issues/838) - Display details of all objects being edited/deleted in bulk * [#1041](https://github.com/netbox-community/netbox/issues/1041) - Added enabled and MTU fields to the interface model @@ -132,7 +134,7 @@ The [NAPALM automation](https://napalm-automation.net/) library provides an abst * [#1269](https://github.com/netbox-community/netbox/issues/1269) - Added circuit termination to interface serializer * [#1320](https://github.com/netbox-community/netbox/issues/1320) - Removed checkbox from confirmation dialog -## Bug Fixes +### Bug Fixes * [#1079](https://github.com/netbox-community/netbox/issues/1079) - Order interfaces naturally via API * [#1285](https://github.com/netbox-community/netbox/issues/1285) - Enforce model validation when creating/editing objects via the API @@ -140,7 +142,7 @@ The [NAPALM automation](https://napalm-automation.net/) library provides an abst * [#1362](https://github.com/netbox-community/netbox/issues/1362) - Raise validation error when attempting to create an API key that's too short * [#1371](https://github.com/netbox-community/netbox/issues/1371) - Extend DeviceSerializer.parent_device to include standard fields -## API changes +### API changes * Added a new API endpoint which makes [NAPALM](https://github.com/napalm-automation/napalm) accessible via NetBox * Device components (console ports, power ports, interfaces, etc.) can only be filtered by a single device name or ID. This limitation was necessary to allow the natural ordering of interfaces according to the device's parent device type. diff --git a/docs/release-notes/version-2.2.md b/docs/release-notes/version-2.2.md index 1e5815c73..905b7a8d1 100644 --- a/docs/release-notes/version-2.2.md +++ b/docs/release-notes/version-2.2.md @@ -1,6 +1,8 @@ -# v2.2.10 (2018-02-21) +# NetBox v2.2 Release Notes -## Enhancements +## v2.2.10 (2018-02-21) + +### Enhancements * [#78](https://github.com/netbox-community/netbox/issues/78) - Extended topology maps to support console and power connections * [#1693](https://github.com/netbox-community/netbox/issues/1693) - Allow specifying loose or exact matching for custom field filters @@ -8,7 +10,7 @@ * [#1876](https://github.com/netbox-community/netbox/issues/1876) - Added explanatory title text to disabled NAPALM buttons on device view * [#1885](https://github.com/netbox-community/netbox/issues/1885) - Added a device filter field for primary IP -## Bug Fixes +### Bug Fixes * [#1858](https://github.com/netbox-community/netbox/issues/1858) - Include device/VM count for cluster list in global search results * [#1859](https://github.com/netbox-community/netbox/issues/1859) - Implemented support for line breaks within CSV fields @@ -18,9 +20,9 @@ --- -# v2.2.9 (2018-01-31) +## v2.2.9 (2018-01-31) -## Enhancements +### Enhancements * [#144](https://github.com/netbox-community/netbox/issues/144) - Implemented bulk import/edit/delete views for InventoryItems * [#1073](https://github.com/netbox-community/netbox/issues/1073) - Include prefixes/IPs from all VRFs when viewing the children of a container prefix in the global table @@ -29,7 +31,7 @@ * [#1824](https://github.com/netbox-community/netbox/issues/1824) - Add virtual machine count to platforms list * [#1835](https://github.com/netbox-community/netbox/issues/1835) - Consistent positioning of previous/next rack buttons -## Bug Fixes +### Bug Fixes * [#1621](https://github.com/netbox-community/netbox/issues/1621) - Tweaked LLDP interface name evaluation logic * [#1765](https://github.com/netbox-community/netbox/issues/1765) - Improved rendering of null options for model choice fields in filter forms @@ -41,9 +43,9 @@ --- -# v2.2.8 (2017-12-20) +## v2.2.8 (2017-12-20) -## Enhancements +### Enhancements * [#1771](https://github.com/netbox-community/netbox/issues/1771) - Added name filter for racks * [#1772](https://github.com/netbox-community/netbox/issues/1772) - Added position filter for devices @@ -51,7 +53,7 @@ * [#1774](https://github.com/netbox-community/netbox/issues/1774) - Include a button to refine search results for all object types under global search * [#1784](https://github.com/netbox-community/netbox/issues/1784) - Added `cluster_type` filters for virtual machines -## Bug Fixes +### Bug Fixes * [#1766](https://github.com/netbox-community/netbox/issues/1766) - Fixed display of "select all" button on device power outlets list * [#1767](https://github.com/netbox-community/netbox/issues/1767) - Use proper template for 404 responses @@ -62,14 +64,14 @@ --- -# v2.2.7 (2017-12-07) +## v2.2.7 (2017-12-07) -## Enhancements +### Enhancements * [#1722](https://github.com/netbox-community/netbox/issues/1722) - Added virtual machine count to site view * [#1737](https://github.com/netbox-community/netbox/issues/1737) - Added a `contains` API filter to find all prefixes containing a given IP or prefix -## Bug Fixes +### Bug Fixes * [#1712](https://github.com/netbox-community/netbox/issues/1712) - Corrected tenant inheritance for new IP addresses created from a parent prefix * [#1721](https://github.com/netbox-community/netbox/issues/1721) - Differentiated child IP count from utilization percentage for prefixes @@ -81,13 +83,13 @@ --- -# v2.2.6 (2017-11-16) +## v2.2.6 (2017-11-16) -## Enhancements +### Enhancements * [#1669](https://github.com/netbox-community/netbox/issues/1669) - Clicking "add an IP" from the prefix view will default to the first available IP within the prefix -## Bug Fixes +### Bug Fixes * [#1397](https://github.com/netbox-community/netbox/issues/1397) - Display global search in navigation menu unless display is less than 1200px wide * [#1599](https://github.com/netbox-community/netbox/issues/1599) - Reduce mobile cut-off for navigation menu to 960px @@ -97,16 +99,16 @@ --- -# v2.2.5 (2017-11-14) +## v2.2.5 (2017-11-14) -## Enhancements +### Enhancements * [#1512](https://github.com/netbox-community/netbox/issues/1512) - Added a view to search for an IP address being assigned to an interface * [#1679](https://github.com/netbox-community/netbox/issues/1679) - Added IP address roles to device/VM interface lists * [#1683](https://github.com/netbox-community/netbox/issues/1683) - Replaced default 500 handler with custom middleware to provide preliminary troubleshooting assistance * [#1684](https://github.com/netbox-community/netbox/issues/1684) - Replaced prefix `parent` filter with `within` and `within_include` -## Bug Fixes +### Bug Fixes * [#1471](https://github.com/netbox-community/netbox/issues/1471) - Correct bulk selection of IP addresses within a prefix assigned to a VRF * [#1642](https://github.com/netbox-community/netbox/issues/1642) - Validate device type classification when creating console server ports and power outlets @@ -121,17 +123,17 @@ --- -# v2.2.4 (2017-10-31) +## v2.2.4 (2017-10-31) -## Bug Fixes +### Bug Fixes * [#1670](https://github.com/netbox-community/netbox/issues/1670) - Fixed server error when calling certain filters (regression from #1649) --- -# v2.2.3 (2017-10-31) +## v2.2.3 (2017-10-31) -## Enhancements +### Enhancements * [#999](https://github.com/netbox-community/netbox/issues/999) - Display devices on which circuits are terminated in circuits list * [#1491](https://github.com/netbox-community/netbox/issues/1491) - Added initial data for the virtualization app @@ -139,7 +141,7 @@ * [#1631](https://github.com/netbox-community/netbox/issues/1631) - Added a `post_run` method to the Report class * [#1666](https://github.com/netbox-community/netbox/issues/1666) - Allow modifying the owner of a rack reservation -## Bug Fixes +### Bug Fixes * [#1513](https://github.com/netbox-community/netbox/issues/1513) - Correct filtering of custom field choices * [#1603](https://github.com/netbox-community/netbox/issues/1603) - Hide selection checkboxes for tables with no available actions @@ -153,14 +155,14 @@ --- -# v2.2.2 (2017-10-17) +## v2.2.2 (2017-10-17) -## Enhancements +### Enhancements * [#1580](https://github.com/netbox-community/netbox/issues/1580) - Allow cluster assignment when bulk importing devices * [#1587](https://github.com/netbox-community/netbox/issues/1587) - Add primary IP column for virtual machines in global search results -## Bug Fixes +### Bug Fixes * [#1498](https://github.com/netbox-community/netbox/issues/1498) - Avoid duplicating nodes when generating topology maps * [#1579](https://github.com/netbox-community/netbox/issues/1579) - Devices already assigned to a cluster cannot be added to a different cluster @@ -172,31 +174,31 @@ --- -# v2.2.1 (2017-10-12) +## v2.2.1 (2017-10-12) -## Bug Fixes +### Bug Fixes * [#1576](https://github.com/netbox-community/netbox/issues/1576) - Moved PostgreSQL validation logic into the relevant migration (fixed ImproperlyConfigured exception on init) --- -# v2.2.0 (2017-10-12) +## v2.2.0 (2017-10-12) **Note:** This release requires PostgreSQL 9.4 or higher. Do not attempt to upgrade unless you are running at least PostgreSQL 9.4. **Note:** The release replaces the deprecated pycrypto library with [pycryptodome](https://github.com/Legrandin/pycryptodome). The upgrade script has been extended to automatically uninstall the old library, but please verify your installed packages with `pip freeze | grep pycrypto` if you run into problems. -## New Features +### New Features -### Virtual Machines and Clusters ([#142](https://github.com/netbox-community/netbox/issues/142)) +#### Virtual Machines and Clusters ([#142](https://github.com/netbox-community/netbox/issues/142)) Our second-most popular feature request has arrived! NetBox now supports the creation of virtual machines, which can be assigned virtual interfaces and IP addresses. VMs are arranged into clusters, each of which has a type and (optionally) a group. -### Custom Validation Reports ([#1511](https://github.com/netbox-community/netbox/issues/1511)) +#### Custom Validation Reports ([#1511](https://github.com/netbox-community/netbox/issues/1511)) Users can now create custom reports which are run to validate data in NetBox. Reports work very similar to Python unit tests: Each report inherits from NetBox's Report class and contains one or more test method. Reports can be run and retrieved via the web UI, API, or CLI. See [the docs](http://netbox.readthedocs.io/en/stable/miscellaneous/reports/) for more info. -## Enhancements +### Enhancements * [#494](https://github.com/netbox-community/netbox/issues/494) - Include asset tag in device info pop-up on rack elevation * [#1444](https://github.com/netbox-community/netbox/issues/1444) - Added a `serial` field to the rack model @@ -208,14 +210,14 @@ Users can now create custom reports which are run to validate data in NetBox. Re * [#1556](https://github.com/netbox-community/netbox/issues/1556) - Added CPAK, CFP2, and CFP4 100GE interface form factors * Added CSV import views for all object types -## Bug Fixes +### Bug Fixes * [#1550](https://github.com/netbox-community/netbox/issues/1550) - Corrected interface connections link in navigation menu * [#1554](https://github.com/netbox-community/netbox/issues/1554) - Don't require form_factor when creating an interface assigned to a virtual machine * [#1557](https://github.com/netbox-community/netbox/issues/1557) - Added filtering for virtual machine interfaces * [#1567](https://github.com/netbox-community/netbox/issues/1567) - Prompt user for session key when importing secrets -## API Changes +### API Changes * Introduced the virtualization app and its associated endpoints at `/api/virtualization` * Added the `/api/extras/reports` endpoint for fetching and running reports diff --git a/docs/release-notes/version-2.3.md b/docs/release-notes/version-2.3.md index 419adc9ac..810999b0b 100644 --- a/docs/release-notes/version-2.3.md +++ b/docs/release-notes/version-2.3.md @@ -1,10 +1,12 @@ -# v2.3.7 (2018-07-26) +# NetBox v2.3 Release Notes -## Enhancements +## v2.3.7 (2018-07-26) + +### Enhancements * [#2166](https://github.com/netbox-community/netbox/issues/2166) - Enable partial matching on device asset_tag during search -## Bug Fixes +### Bug Fixes * [#1977](https://github.com/netbox-community/netbox/issues/1977) - Fixed exception when creating a virtual chassis with a non-master device in position 1 * [#1992](https://github.com/netbox-community/netbox/issues/1992) - Isolate errors when one of multiple NAPALM methods fails @@ -19,29 +21,29 @@ --- -# v2.3.6 (2018-07-16) +## v2.3.6 (2018-07-16) -## Enhancements +### Enhancements * [#2107](https://github.com/netbox-community/netbox/issues/2107) - Added virtual chassis to global search * [#2125](https://github.com/netbox-community/netbox/issues/2125) - Show child status in device bay list -## Bug Fixes +### Bug Fixes * [#2214](https://github.com/netbox-community/netbox/issues/2214) - Error when assigning a VLAN to an interface on a VM in a cluster with no assigned site * [#2239](https://github.com/netbox-community/netbox/issues/2239) - Pin django-filter to version 1.1.0 --- -# v2.3.5 (2018-07-02) +## v2.3.5 (2018-07-02) -## Enhancements +### Enhancements * [#2159](https://github.com/netbox-community/netbox/issues/2159) - Allow custom choice field to specify a default choice * [#2177](https://github.com/netbox-community/netbox/issues/2177) - Include device serial number in rack elevation pop-up * [#2194](https://github.com/netbox-community/netbox/issues/2194) - Added `address` filter to IPAddress model -## Bug Fixes +### Bug Fixes * [#1826](https://github.com/netbox-community/netbox/issues/1826) - Corrected description of security parameters under API definition * [#2021](https://github.com/netbox-community/netbox/issues/2021) - Fix recursion error when viewing API docs under Python 3.4 @@ -54,9 +56,9 @@ --- -# v2.3.4 (2018-06-07) +## v2.3.4 (2018-06-07) -## Bug Fixes +### Bug Fixes * [#2066](https://github.com/netbox-community/netbox/issues/2066) - Catch `AddrFormatError` exception on invalid IP addresses * [#2075](https://github.com/netbox-community/netbox/issues/2075) - Enable tenant assignment when creating a rack reservation via the API @@ -72,13 +74,13 @@ --- -# v2.3.3 (2018-04-19) +## v2.3.3 (2018-04-19) -## Enhancements +### Enhancements * [#1990](https://github.com/netbox-community/netbox/issues/1990) - Improved search function when assigning an IP address to an interface -## Bug Fixes +### Bug Fixes * [#1975](https://github.com/netbox-community/netbox/issues/1975) - Correct filtering logic for custom boolean fields * [#1988](https://github.com/netbox-community/netbox/issues/1988) - Order interfaces naturally when bulk renaming @@ -93,9 +95,9 @@ --- -# v2.3.2 (2018-03-22) +## v2.3.2 (2018-03-22) -## Enhancements +### Enhancements * [#1586](https://github.com/netbox-community/netbox/issues/1586) - Extend bulk interface creation to support alphanumeric characters * [#1866](https://github.com/netbox-community/netbox/issues/1866) - Introduced AnnotatedMultipleChoiceField for filter forms @@ -105,7 +107,7 @@ * [#1949](https://github.com/netbox-community/netbox/issues/1949) - Added a button to view elevations on rack groups list * [#1952](https://github.com/netbox-community/netbox/issues/1952) - Implemented a more robust mechanism for assigning VLANs to interfaces -## Bug Fixes +### Bug Fixes * [#1948](https://github.com/netbox-community/netbox/issues/1948) - Fix TypeError when attempting to add a member to an existing virtual chassis * [#1951](https://github.com/netbox-community/netbox/issues/1951) - Fix TypeError exception when importing platforms @@ -116,13 +118,13 @@ --- -# v2.3.1 (2018-03-01) +## v2.3.1 (2018-03-01) -## Enhancements +### Enhancements * [#1910](https://github.com/netbox-community/netbox/issues/1910) - Added filters for cluster group and cluster type -## Bug Fixes +### Bug Fixes * [#1915](https://github.com/netbox-community/netbox/issues/1915) - Redirect to device view after deleting a component * [#1919](https://github.com/netbox-community/netbox/issues/1919) - Prevent exception when attempting to create a virtual machine without selecting devices @@ -137,19 +139,19 @@ --- -# v2.3.0 (2018-02-26) +## v2.3.0 (2018-02-26) -## New Features +### New Features -### Virtual Chassis ([#99](https://github.com/netbox-community/netbox/issues/99)) +#### Virtual Chassis ([#99](https://github.com/netbox-community/netbox/issues/99)) A virtual chassis represents a set of physical devices with a shared control plane; for example, a stack of switches managed as a single device. Viewing the master device of a virtual chassis will show all member interfaces and IP addresses. -### Interface VLAN Assignments ([#150](https://github.com/netbox-community/netbox/issues/150)) +#### Interface VLAN Assignments ([#150](https://github.com/netbox-community/netbox/issues/150)) Interfaces can now be assigned an 802.1Q mode (access or trunked) and associated with particular VLANs. Thanks to [John Anderson](https://github.com/lampwins) for his work on this! -### Bulk Object Creation via the API ([#1553](https://github.com/netbox-community/netbox/issues/1553)) +#### Bulk Object Creation via the API ([#1553](https://github.com/netbox-community/netbox/issues/1553)) The REST API now supports the creation of multiple objects of the same type using a single POST request. For example, to create multiple devices: @@ -163,7 +165,7 @@ curl -X POST -H "Authorization: Token " -H "Content-Type: application/jso Bulk creation is all-or-none: If any of the creations fails, the entire operation is rolled back. -### Automatic Provisioning of Next Available Prefixes ([#1694](https://github.com/netbox-community/netbox/issues/1694)) +#### Automatic Provisioning of Next Available Prefixes ([#1694](https://github.com/netbox-community/netbox/issues/1694)) Similar to IP addresses, NetBox now supports automated provisioning of available prefixes from within a parent prefix. For example, to retrieve the next three available /28s within a parent /24: @@ -177,11 +179,11 @@ curl -X POST -H "Authorization: Token " -H "Content-Type: application/jso If the parent prefix cannot accommodate all requested prefixes, the operation is cancelled and no new prefixes are created. -### Bulk Renaming of Device/VM Components ([#1781](https://github.com/netbox-community/netbox/issues/1781)) +#### Bulk Renaming of Device/VM Components ([#1781](https://github.com/netbox-community/netbox/issues/1781)) Device components (interfaces, console ports, etc.) can now be renamed in bulk via the web interface. This was implemented primarily to support the bulk renumbering of interfaces whose parent is part of a virtual chassis. -## Enhancements +### Enhancements * [#1283](https://github.com/netbox-community/netbox/issues/1283) - Added a `time_zone` field to the site model * [#1321](https://github.com/netbox-community/netbox/issues/1321) - Added `created` and `last_updated` fields for relevant models to their API serializers @@ -192,7 +194,7 @@ Device components (interfaces, console ports, etc.) can now be renamed in bulk v * [#1821](https://github.com/netbox-community/netbox/issues/1821) - Added a `description` field to the site model * [#1864](https://github.com/netbox-community/netbox/issues/1864) - Added a `status` field to the circuit model -## Bug Fixes +### Bug Fixes * [#1136](https://github.com/netbox-community/netbox/issues/1136) - Enforce model validation during bulk update * [#1645](https://github.com/netbox-community/netbox/issues/1645) - Simplified interface serialzier for IP addresses and optimized API view queryset @@ -206,11 +208,11 @@ Device components (interfaces, console ports, etc.) can now be renamed in bulk v \* New since v2.3-beta2 -## Breaking Changes +### Breaking Changes * Constants representing device status have been renamed for clarity (for example, `STATUS_ACTIVE` is now `DEVICE_STATUS_ACTIVE`). Custom validation reports will need to be updated if they reference any of these constants. -## API Changes +### API Changes * API creation calls now accept either a single JSON object or a list of JSON objects. If multiple objects are passed and one or more them fail validation, no objects will be created. * Added `created` and `last_updated` fields for objects inheriting from CreatedUpdatedModel. diff --git a/docs/release-notes/version-2.4.md b/docs/release-notes/version-2.4.md index b477ba9db..5a55a6358 100644 --- a/docs/release-notes/version-2.4.md +++ b/docs/release-notes/version-2.4.md @@ -1,13 +1,14 @@ +# NetBox v2.4 Release Notes -# v2.4.9 (2018-12-07) +## v2.4.9 (2018-12-07) -## Enhancements +### Enhancements * [#2089](https://github.com/netbox-community/netbox/issues/2089) - Add SONET interface form factors * [#2495](https://github.com/netbox-community/netbox/issues/2495) - Enable deep-merging of config context data * [#2597](https://github.com/netbox-community/netbox/issues/2597) - Add FibreChannel SFP28 (32GFC) interface form factor -## Bug Fixes +### Bug Fixes * [#2400](https://github.com/netbox-community/netbox/issues/2400) - Correct representation of nested object assignment in API docs * [#2576](https://github.com/netbox-community/netbox/issues/2576) - Correct type for count_* fields in site API representation @@ -20,14 +21,14 @@ --- -# v2.4.8 (2018-11-20) +## v2.4.8 (2018-11-20) -## Enhancements +### Enhancements * [#2490](https://github.com/netbox-community/netbox/issues/2490) - Added bulk editing for config contexts * [#2557](https://github.com/netbox-community/netbox/issues/2557) - Added object view for tags -## Bug Fixes +### Bug Fixes * [#2473](https://github.com/netbox-community/netbox/issues/2473) - Fix encoding of long (>127 character) secrets * [#2558](https://github.com/netbox-community/netbox/issues/2558) - Filter on all tags when multiple are passed @@ -38,15 +39,15 @@ --- -# v2.4.7 (2018-11-06) +## v2.4.7 (2018-11-06) -## Enhancements +### Enhancements * [#2388](https://github.com/netbox-community/netbox/issues/2388) - Enable filtering of devices/VMs by region * [#2427](https://github.com/netbox-community/netbox/issues/2427) - Allow filtering of interfaces by assigned VLAN or VLAN ID * [#2512](https://github.com/netbox-community/netbox/issues/2512) - Add device field to inventory item filter form -## Bug Fixes +### Bug Fixes * [#2502](https://github.com/netbox-community/netbox/issues/2502) - Allow duplicate VIPs inside a uniqueness-enforced VRF * [#2514](https://github.com/netbox-community/netbox/issues/2514) - Prevent new connections to already connected interfaces @@ -56,14 +57,14 @@ --- -# v2.4.6 (2018-10-05) +## v2.4.6 (2018-10-05) -## Enhancements +### Enhancements * [#2479](https://github.com/netbox-community/netbox/issues/2479) - Add user permissions for creating/modifying API tokens * [#2487](https://github.com/netbox-community/netbox/issues/2487) - Return abbreviated API output when passed `?brief=1` -## Bug Fixes +### Bug Fixes * [#2393](https://github.com/netbox-community/netbox/issues/2393) - Fix Unicode support for CSV import under Python 2 * [#2483](https://github.com/netbox-community/netbox/issues/2483) - Set max item count of API-populated form fields to MAX_PAGE_SIZE @@ -74,16 +75,16 @@ --- -# v2.4.5 (2018-10-02) +## v2.4.5 (2018-10-02) -## Enhancements +### Enhancements * [#2392](https://github.com/netbox-community/netbox/issues/2392) - Implemented local context data for devices and virtual machines * [#2402](https://github.com/netbox-community/netbox/issues/2402) - Order and format JSON data in form fields * [#2432](https://github.com/netbox-community/netbox/issues/2432) - Link remote interface connections to the Interface view * [#2438](https://github.com/netbox-community/netbox/issues/2438) - API optimizations for tagged objects -## Bug Fixes +### Bug Fixes * [#2406](https://github.com/netbox-community/netbox/issues/2406) - Remove hard-coded limit of 1000 objects from API-populated form fields * [#2414](https://github.com/netbox-community/netbox/issues/2414) - Tags field missing from device/VM component creation forms @@ -95,16 +96,16 @@ --- -# v2.4.4 (2018-08-22) +## v2.4.4 (2018-08-22) -## Enhancements +### Enhancements * [#2168](https://github.com/netbox-community/netbox/issues/2168) - Added Extreme SummitStack interface form factors * [#2356](https://github.com/netbox-community/netbox/issues/2356) - Include cluster site as read-only field in VirtualMachine serializer * [#2362](https://github.com/netbox-community/netbox/issues/2362) - Implemented custom admin site to properly handle BASE_PATH * [#2254](https://github.com/netbox-community/netbox/issues/2254) - Implemented searchability for Rack Groups -## Bug Fixes +### Bug Fixes * [#2353](https://github.com/netbox-community/netbox/issues/2353) - Handle `DoesNotExist` exception when deleting a device with connected interfaces * [#2354](https://github.com/netbox-community/netbox/issues/2354) - Increased maximum MTU for interfaces to 65536 bytes @@ -117,13 +118,13 @@ --- -# v2.4.3 (2018-08-09) +## v2.4.3 (2018-08-09) -## Enhancements +### Enhancements * [#2333](https://github.com/netbox-community/netbox/issues/2333) - Added search filters for ConfigContexts -## Bug Fixes +### Bug Fixes * [#2334](https://github.com/netbox-community/netbox/issues/2334) - TypeError raised when WritableNestedSerializer receives a non-integer value * [#2335](https://github.com/netbox-community/netbox/issues/2335) - API requires group field when creating/updating a rack @@ -135,9 +136,9 @@ --- -# v2.4.2 (2018-08-08) +## v2.4.2 (2018-08-08) -## Bug Fixes +### Bug Fixes * [#2318](https://github.com/netbox-community/netbox/issues/2318) - ImportError when viewing a report * [#2319](https://github.com/netbox-community/netbox/issues/2319) - Extend ChoiceField to properly handle true/false choice keys @@ -149,9 +150,9 @@ --- -# v2.4.1 (2018-08-07) +## v2.4.1 (2018-08-07) -## Bug Fixes +### Bug Fixes * [#2303](https://github.com/netbox-community/netbox/issues/2303) - Always redirect to parent object when bulk editing/deleting components * [#2308](https://github.com/netbox-community/netbox/issues/2308) - Custom fields panel absent from object view in UI @@ -162,29 +163,29 @@ --- -# v2.4.0 (2018-08-06) +## v2.4.0 (2018-08-06) -## New Features +### New Features -### Webhooks ([#81](https://github.com/netbox-community/netbox/issues/81)) +#### Webhooks ([#81](https://github.com/netbox-community/netbox/issues/81)) Webhooks enable NetBox to send a representation of an object every time one is created, updated, or deleted. Webhooks are sent from NetBox to external services via HTTP, and can be limited by object type. Services which receive a webhook can act on the data provided by NetBox to automate other tasks. Special thanks to [John Anderson](https://github.com/lampwins) for doing the heavy lifting for this feature! -### Tagging ([#132](https://github.com/netbox-community/netbox/issues/132)) +#### Tagging ([#132](https://github.com/netbox-community/netbox/issues/132)) Tags are free-form labels which can be assigned to a variety of objects in NetBox. Tags can be used to categorize and filter objects in addition to built-in and custom fields. Objects to which tags apply now include a `tags` field in the API. -### Contextual Configuration Data ([#1349](https://github.com/netbox-community/netbox/issues/1349)) +#### Contextual Configuration Data ([#1349](https://github.com/netbox-community/netbox/issues/1349)) Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. (For example, you might want to associate a set of syslog servers for all devices at a particular site.) Context data enables the association of arbitrary data (expressed in JSON format) to devices and virtual machines grouped by region, site, role, platform, and/or tenancy. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object. -### Change Logging ([#1898](https://github.com/netbox-community/netbox/issues/1898)) +#### Change Logging ([#1898](https://github.com/netbox-community/netbox/issues/1898)) When an object is created, updated, or deleted, NetBox now automatically records a serialized representation of that object (similar to how it appears in the REST API) as well the event time and user account associated with the change. -## Enhancements +### Enhancements * [#238](https://github.com/netbox-community/netbox/issues/238) - Allow racks with the same name within a site (but in different groups) * [#971](https://github.com/netbox-community/netbox/issues/971) - Add a view to show all VLAN IDs available within a group @@ -199,12 +200,12 @@ When an object is created, updated, or deleted, NetBox now automatically records * [#2157](https://github.com/netbox-community/netbox/issues/2157) - Fixed natural ordering of objects when sorted by name * [#2225](https://github.com/netbox-community/netbox/issues/2225) - Add "view elevations" button for site rack groups -## Bug Fixes +### Bug Fixes * [#2272](https://github.com/netbox-community/netbox/issues/2272) - Allow subdevice_role to be null on DeviceTypeSerializer" * [#2286](https://github.com/netbox-community/netbox/issues/2286) - Fixed "mark connected" button for PDU outlet connections -## API Changes +### API Changes * Introduced the `/extras/config-contexts/`, `/extras/object-changes/`, and `/extras/tags/` API endpoints * API writes now return a nested representation of related objects (rather than only a numeric ID) @@ -213,15 +214,15 @@ When an object is created, updated, or deleted, NetBox now automatically records * The ipam.Service and secrets.Secret serializers now include custom fields * The dcim.Platform serializer now includes a free-form (JSON) `napalm_args` field -## Changes Since v2.4-beta1 +### Changes Since v2.4-beta1 -### Enhancements +#### Enhancements * [#2229](https://github.com/netbox-community/netbox/issues/2229) - Allow mapping of ConfigContexts to tenant groups * [#2259](https://github.com/netbox-community/netbox/issues/2259) - Add changelog tab to interface view * [#2264](https://github.com/netbox-community/netbox/issues/2264) - Added "map it" link for site GPS coordinates -### Bug Fixes +#### Bug Fixes * [#2137](https://github.com/netbox-community/netbox/issues/2137) - Fixed JSON serialization of dates * [#2258](https://github.com/netbox-community/netbox/issues/2258) - Include changed object type on home page changelog diff --git a/docs/release-notes/version-2.5.md b/docs/release-notes/version-2.5.md index 45a1ba0d0..666ecb6f1 100644 --- a/docs/release-notes/version-2.5.md +++ b/docs/release-notes/version-2.5.md @@ -1,6 +1,8 @@ -# v2.5.13 (2019-05-31) +# NetBox v2.5 Release Notes -## Enhancements +## v2.5.13 (2019-05-31) + +### Enhancements * [#2813](https://github.com/netbox-community/netbox/issues/2813) - Add tenant group filters * [#3085](https://github.com/netbox-community/netbox/issues/3085) - Catch all exceptions during export template rendering @@ -11,7 +13,7 @@ * [#3185](https://github.com/netbox-community/netbox/issues/3185) - Improve performance for custom field access within templates * [#3186](https://github.com/netbox-community/netbox/issues/3186) - Add interface name filter for IP addresses -## Bug Fixes +### Bug Fixes * [#3031](https://github.com/netbox-community/netbox/issues/3031) - Fixed form field population of tags with spaces * [#3132](https://github.com/netbox-community/netbox/issues/3132) - Circuit termination missing from available cable termination types @@ -25,9 +27,9 @@ --- -# v2.5.12 (2019-05-01) +## v2.5.12 (2019-05-01) -## Bug Fixes +### Bug Fixes * [#3127](https://github.com/netbox-community/netbox/issues/3127) - Fix natural ordering of device components @@ -35,17 +37,17 @@ 2.5.11 (2019-04-29) -## Notes +### Notes This release upgrades the Django framework to version 2.2. -## Enhancements +### Enhancements * [#2986](https://github.com/netbox-community/netbox/issues/2986) - Improve natural ordering of device components * [#3023](https://github.com/netbox-community/netbox/issues/3023) - Add support for filtering cables by connected device * [#3070](https://github.com/netbox-community/netbox/issues/3070) - Add decommissioning status for devices -## Bug Fixes +### Bug Fixes * [#2621](https://github.com/netbox-community/netbox/issues/2621) - Upgrade Django requirement to 2.2 to fix object deletion issue in the changelog middleware * [#3072](https://github.com/netbox-community/netbox/issues/3072) - Preserve multiselect filter values when updating per-page count for list views @@ -55,13 +57,13 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.10 (2019-04-08) +## v2.5.10 (2019-04-08) -## Enhancements +### Enhancements * [#3052](https://github.com/netbox-community/netbox/issues/3052) - Add Jinja2 support for export templates -## Bug Fixes +### Bug Fixes * [#2937](https://github.com/netbox-community/netbox/issues/2937) - Redirect to list view after editing an object from list view * [#3036](https://github.com/netbox-community/netbox/issues/3036) - DCIM interfaces API endpoint should not include VM interfaces @@ -73,15 +75,15 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.9 (2019-04-01) +## v2.5.9 (2019-04-01) -## Enhancements +### Enhancements * [#2933](https://github.com/netbox-community/netbox/issues/2933) - Add username to outbound webhook requests * [#3011](https://github.com/netbox-community/netbox/issues/3011) - Add SSL support for django-rq (requires django-rq v1.3.1+) * [#3025](https://github.com/netbox-community/netbox/issues/3025) - Add request ID to outbound webhook requests (for correlating all changes part of a single request) -## Bug Fixes +### Bug Fixes * [#2207](https://github.com/netbox-community/netbox/issues/2207) - Fixes deterministic ordering of interfaces * [#2577](https://github.com/netbox-community/netbox/issues/2577) - Clarification of wording in API regarding filtering @@ -98,13 +100,13 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.8 (2019-03-11) +## v2.5.8 (2019-03-11) -## Enhancements +### Enhancements * [#2435](https://github.com/netbox-community/netbox/issues/2435) - Printer friendly CSS -## Bug Fixes +### Bug Fixes * [#2065](https://github.com/netbox-community/netbox/issues/2065) - Correct documentation for VM interface serializer * [#2705](https://github.com/netbox-community/netbox/issues/2705) - Fix endpoint grouping in API docs @@ -128,9 +130,9 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.7 (2019-02-21) +## v2.5.7 (2019-02-21) -## Enhancements +### Enhancements * [#2357](https://github.com/netbox-community/netbox/issues/2357) - Enable filtering of devices by rack face * [#2638](https://github.com/netbox-community/netbox/issues/2638) - Add button to copy unlocked secret to clipboard @@ -140,7 +142,7 @@ This release upgrades the Django framework to version 2.2. * [#2898](https://github.com/netbox-community/netbox/issues/2898) - Enable filtering cables list by connection status * [#2903](https://github.com/netbox-community/netbox/issues/2903) - Clarify purpose of tags field on interface edit form -## Bug Fixes +### Bug Fixes * [#2852](https://github.com/netbox-community/netbox/issues/2852) - Allow filtering devices by null rack position * [#2884](https://github.com/netbox-community/netbox/issues/2884) - Don't display connect button for wireless interfaces @@ -156,16 +158,16 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.6 (2019-02-13) +## v2.5.6 (2019-02-13) -## Enhancements +### Enhancements * [#2758](https://github.com/netbox-community/netbox/issues/2758) - Add cable trace button to pass-through ports * [#2839](https://github.com/netbox-community/netbox/issues/2839) - Add "110 punch" type for pass-through ports * [#2854](https://github.com/netbox-community/netbox/issues/2854) - Enable bulk editing of pass-through ports * [#2866](https://github.com/netbox-community/netbox/issues/2866) - Add cellular interface types (GSM/CDMA/LTE) -## Bug Fixes +### Bug Fixes * [#2841](https://github.com/netbox-community/netbox/issues/2841) - Fix filtering by VRF for prefix and IP address lists * [#2844](https://github.com/netbox-community/netbox/issues/2844) - Correct display of far cable end for pass-through ports @@ -179,15 +181,15 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.5 (2019-01-31) +## v2.5.5 (2019-01-31) -## Enhancements +### Enhancements * [#2805](https://github.com/netbox-community/netbox/issues/2805) - Allow null route distinguisher for VRFs * [#2809](https://github.com/netbox-community/netbox/issues/2809) - Remove VRF child prefixes table; link to main prefixes view * [#2825](https://github.com/netbox-community/netbox/issues/2825) - Include directly connected device for front/rear ports -## Bug Fixes +### Bug Fixes * [#2824](https://github.com/netbox-community/netbox/issues/2824) - Fix template exception when viewing rack elevations list * [#2833](https://github.com/netbox-community/netbox/issues/2833) - Fix form widget for front port template creation @@ -196,9 +198,9 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.4 (2019-01-29) +## v2.5.4 (2019-01-29) -## Enhancements +### Enhancements * [#2516](https://github.com/netbox-community/netbox/issues/2516) - Implemented Select2 for all Model backed selection fields * [#2590](https://github.com/netbox-community/netbox/issues/2590) - Implemented the color picker with Select2 to show colors in the background @@ -211,7 +213,7 @@ This release upgrades the Django framework to version 2.2. * [#2808](https://github.com/netbox-community/netbox/issues/2808) - Loosen version pinning for Django to allow patch releases * [#2810](https://github.com/netbox-community/netbox/issues/2810) - Include description fields in interface connections export -## Bug Fixes +### Bug Fixes * [#2779](https://github.com/netbox-community/netbox/issues/2779) - Include "none" option when filter IP addresses by role * [#2783](https://github.com/netbox-community/netbox/issues/2783) - Fix AttributeError exception when attempting to delete region(s) @@ -222,9 +224,9 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.3 (2019-01-11) +## v2.5.3 (2019-01-11) -## Enhancements +### Enhancements * [#1630](https://github.com/netbox-community/netbox/issues/1630) - Enable bulk editing of prefix/IP mask length * [#1870](https://github.com/netbox-community/netbox/issues/1870) - Add per-page toggle to object lists @@ -234,7 +236,7 @@ This release upgrades the Django framework to version 2.2. * [#2693](https://github.com/netbox-community/netbox/issues/2693) - Additional cable colors * [#2726](https://github.com/netbox-community/netbox/issues/2726) - Include cables in global search -## Bug Fixes +### Bug Fixes * [#2742](https://github.com/netbox-community/netbox/issues/2742) - Preserve cluster assignment when editing a device * [#2757](https://github.com/netbox-community/netbox/issues/2757) - Always treat first/last IPs within a /31 or /127 as usable @@ -244,14 +246,14 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.2 (2018-12-21) +## v2.5.2 (2018-12-21) -## Enhancements +### Enhancements * [#2561](https://github.com/netbox-community/netbox/issues/2561) - Add 200G and 400G interface types * [#2701](https://github.com/netbox-community/netbox/issues/2701) - Enable filtering of prefixes by exact prefix value -## Bug Fixes +### Bug Fixes * [#2673](https://github.com/netbox-community/netbox/issues/2673) - Fix exception on LLDP neighbors view for device with a circuit connected * [#2691](https://github.com/netbox-community/netbox/issues/2691) - Cable trace should follow circuits @@ -266,14 +268,14 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.1 (2018-12-13) +## v2.5.1 (2018-12-13) -## Enhancements +### Enhancements * [#2655](https://github.com/netbox-community/netbox/issues/2655) - Add 128GFC Fibrechannel interface type * [#2674](https://github.com/netbox-community/netbox/issues/2674) - Enable filtering changelog by object type under web UI -## Bug Fixes +### Bug Fixes * [#2662](https://github.com/netbox-community/netbox/issues/2662) - Fix ImproperlyConfigured exception when rendering API docs * [#2663](https://github.com/netbox-community/netbox/issues/2663) - Prevent duplicate interfaces from appearing under VLAN members view @@ -287,33 +289,33 @@ This release upgrades the Django framework to version 2.2. --- -# v2.5.0 (2018-12-10) +## v2.5.0 (2018-12-10) -## Notes +### Notes -### Python 3 Required +#### Python 3 Required As promised, Python 2 support has been completed removed. Python 3.5 or higher is now required to run NetBox. Please see [our Python 3 migration guide](https://netbox.readthedocs.io/en/stable/installation/migrating-to-python3/) for assistance with upgrading. -### Removed Deprecated User Activity Log +#### Removed Deprecated User Activity Log The UserAction model, which was deprecated by the new change logging feature in NetBox v2.4, has been removed. If you need to archive legacy user activity, do so prior to upgrading to NetBox v2.5, as the database migration will remove all data associated with this model. -### View Permissions in Django 2.1 +#### View Permissions in Django 2.1 Django 2.1 introduces view permissions for object types (not to be confused with object-level permissions). Implementation of [#323](https://github.com/netbox-community/netbox/issues/323) is planned for NetBox v2.6. Users are encourage to begin assigning view permissions as desired in preparation for their eventual enforcement. -### upgrade.sh No Longer Invokes sudo +#### upgrade.sh No Longer Invokes sudo The `upgrade.sh` script has been tweaked so that it no longer invokes `sudo` internally. This was done to ensure compatibility when running NetBox inside a Python virtual environment. If you need elevated permissions when upgrading NetBox, call the upgrade script with `sudo upgrade.sh`. -## New Features +### New Features -### Patch Panels and Cables ([#20](https://github.com/netbox-community/netbox/issues/20)) +#### Patch Panels and Cables ([#20](https://github.com/netbox-community/netbox/issues/20)) NetBox now supports modeling physical cables for console, power, and interface connections. The new pass-through port component type has also been introduced to model patch panels and similar devices. -## Enhancements +### Enhancements * [#450](https://github.com/netbox-community/netbox/issues/450) - Added `outer_width` and `outer_depth` fields to rack model * [#867](https://github.com/netbox-community/netbox/issues/867) - Added `description` field to circuit terminations @@ -329,7 +331,7 @@ NetBox now supports modeling physical cables for console, power, and interface c * [#2426](https://github.com/netbox-community/netbox/issues/2426) - Introduced `SESSION_FILE_PATH` configuration setting for authentication without write access to database * [#2594](https://github.com/netbox-community/netbox/issues/2594) - `upgrade.sh` no longer invokes sudo -## Changes From v2.5-beta2 +### Changes From v2.5-beta2 * [#2474](https://github.com/netbox-community/netbox/issues/2474) - Add `cabled` and `connection_status` filters for device components * [#2616](https://github.com/netbox-community/netbox/issues/2616) - Convert Rack `outer_unit` and Cable `length_unit` to integer-based choice fields @@ -341,7 +343,7 @@ NetBox now supports modeling physical cables for console, power, and interface c * [#2648](https://github.com/netbox-community/netbox/issues/2648) - Include the `connection_status` field in nested represenations of connectable device components * [#2649](https://github.com/netbox-community/netbox/issues/2649) - Add `connected_endpoint_type` to connectable device component API representations -## API Changes +### API Changes * The `/extras/recent-activity/` endpoint (replaced by change logging in v2.4) has been removed * The `rpc_client` field has been removed from dcim.Platform (see #2367) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 9fd258b0f..e8228920d 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,6 +1,8 @@ -# v2.6.12 (2020-01-13) +# NetBox v2.6 Release Notes -## Enhancements +## 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 @@ -18,7 +20,7 @@ * [#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 +### 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 @@ -35,18 +37,18 @@ --- -# v2.6.11 (2020-01-03) +## v2.6.11 (2020-01-03) -## Bug Fixes +### Bug Fixes * [#3831](https://github.com/netbox-community/netbox/issues/3831) - Fix API-driven filter field rendering (#3812 regression) * [#3833](https://github.com/netbox-community/netbox/issues/3833) - Add missing region filters for multiple objects --- -# v2.6.10 (2020-01-02) +## v2.6.10 (2020-01-02) -## Enhancements +### Enhancements * [#2233](https://github.com/netbox-community/netbox/issues/2233) - Add ability to move inventory items between devices * [#2892](https://github.com/netbox-community/netbox/issues/2892) - Extend admin UI to allow deleting old report results @@ -58,7 +60,7 @@ * [#3812](https://github.com/netbox-community/netbox/issues/3812) - Optimize size of pages containing a dynamic selection field * [#3827](https://github.com/netbox-community/netbox/issues/3827) - Allow filtering console/power/interface connections by device ID -## Bug Fixes +### Bug Fixes * [#3106](https://github.com/netbox-community/netbox/issues/3106) - Restrict queryset of chained fields when form validation fails * [#3695](https://github.com/netbox-community/netbox/issues/3695) - Include A/Z termination sites for circuits in global search @@ -70,15 +72,15 @@ --- -# v2.6.9 (2019-12-16) +## v2.6.9 (2019-12-16) -## Enhancements +### Enhancements * [#3152](https://github.com/netbox-community/netbox/issues/3152) - Include direct link to rack elevations on site view * [#3441](https://github.com/netbox-community/netbox/issues/3441) - Move virtual machine results near devices in global search * [#3761](https://github.com/netbox-community/netbox/issues/3761) - Added copy button for API tokens -## Bug Fixes +### Bug Fixes * [#2170](https://github.com/netbox-community/netbox/issues/2170) - Prevent the deletion of a virtual chassis when a cross-member LAG is present * [#2358](https://github.com/netbox-community/netbox/issues/2358) - Respect custom field default values when creating objects via the REST API @@ -87,9 +89,9 @@ --- -# v2.6.8 (2019-12-10) +## v2.6.8 (2019-12-10) -## Enhancements +### Enhancements * [#3139](https://github.com/netbox-community/netbox/issues/3139) - Disable password change form for LDAP-authenticated users * [#3457](https://github.com/netbox-community/netbox/issues/3457) - Display cable colors on device view @@ -97,7 +99,7 @@ * [#3663](https://github.com/netbox-community/netbox/issues/3663) - Add query filters for `created` and `last_updated` fields * [#3722](https://github.com/netbox-community/netbox/issues/3722) - Allow the underscore character in IPAddress DNS names -## Bug Fixes +### Bug Fixes * [#3312](https://github.com/netbox-community/netbox/issues/3312) - Fix validation error when editing power cables in bulk * [#3644](https://github.com/netbox-community/netbox/issues/3644) - Fix exception when connecting a cable to a RearPort with no corresponding FrontPort @@ -111,9 +113,9 @@ --- -# v2.6.7 (2019-11-01) +## v2.6.7 (2019-11-01) -## Enhancements +### Enhancements * [#3445](https://github.com/netbox-community/netbox/issues/3445) - Add support for additional user defined headers to be added to webhook requests * [#3499](https://github.com/netbox-community/netbox/issues/3499) - Add `ca_file_path` to Webhook model to support user supplied CA certificate verification of webhook requests @@ -121,7 +123,7 @@ * [#3619](https://github.com/netbox-community/netbox/issues/3619) - Add 400GE OSFP interface type * [#3659](https://github.com/netbox-community/netbox/issues/3659) - Add filtering for objects in admin UI -## Bug Fixes +### Bug Fixes * [#3309](https://github.com/netbox-community/netbox/issues/3309) - Rewrite change logging middleware to resolve sporadic testing failures * [#3340](https://github.com/netbox-community/netbox/issues/3340) - Add missing options to connect front ports to console ports @@ -135,13 +137,13 @@ --- -# v2.6.6 (2019-10-10) +## v2.6.6 (2019-10-10) -## Notes +### Notes * This release includes a migration which automatically updates all existing cables to enable filtering by site/rack (see [#3259](https://github.com/netbox-community/netbox/issues/3259)). This migration may take several minutes to complete on installations with tens of thousands of cables defined. -## Enhancements +### Enhancements * [#1941](https://github.com/netbox-community/netbox/issues/1941) - Add InfiniBand interface types * [#3259](https://github.com/netbox-community/netbox/issues/3259) - Add `rack` and `site` filters for cables @@ -151,7 +153,7 @@ * [#3580](https://github.com/netbox-community/netbox/issues/3580) - Render text and URL fields as textareas in the custom link form * [#3581](https://github.com/netbox-community/netbox/issues/3581) - Introduce `commit_default` custom script attribute to not commit changes by default -## Bug Fixes +### Bug Fixes * [#3458](https://github.com/netbox-community/netbox/issues/3458) - Prevent primary IP address for a device/VM from being reassigned * [#3463](https://github.com/netbox-community/netbox/issues/3463) - Correct CSV headers for exported power feeds @@ -165,9 +167,9 @@ --- -# v2.6.5 (2019-09-25) +## v2.6.5 (2019-09-25) -## Enhancements +### Enhancements * [#3297](https://github.com/netbox-community/netbox/issues/3297) - Include reserved units when calculating rack utilization * [#3347](https://github.com/netbox-community/netbox/issues/3347) - Extend upgrade script to automatically remove stale content types @@ -176,7 +178,7 @@ * [#3524](https://github.com/netbox-community/netbox/issues/3524) - Enable bulk editing of power outlet/power port associations * [#3529](https://github.com/netbox-community/netbox/issues/3529) - Enable filtering circuits list by region -## Bug Fixes +### Bug Fixes * [#3435](https://github.com/netbox-community/netbox/issues/3435) - Change IP/prefix CSV export to reference VRF name instead of RD * [#3464](https://github.com/netbox-community/netbox/issues/3464) - Fix foreground text color on color picker fields @@ -189,9 +191,9 @@ --- -# v2.6.4 (2019-09-19) +## v2.6.4 (2019-09-19) -## Enhancements +### Enhancements * [#2160](https://github.com/netbox-community/netbox/issues/2160) - Add bulk editing for interface VLAN assignment * [#3027](https://github.com/netbox-community/netbox/issues/3028) - Add `local_context_data` boolean filter for devices @@ -200,7 +202,7 @@ * [#3485](https://github.com/netbox-community/netbox/issues/3485) - Enable embedded graphs for devices * [#3510](https://github.com/netbox-community/netbox/issues/3510) - Add minimum/maximum prefix length enforcement for `IPNetworkVar` -## Bug Fixes +### Bug Fixes * [#3489](https://github.com/netbox-community/netbox/issues/3489) - Prevent exception triggered by webhook upon object deletion * [#3501](https://github.com/netbox-community/netbox/issues/3501) - Fix rendering of checkboxes on custom script forms @@ -210,17 +212,17 @@ --- -# v2.6.3 (2019-09-04) +## v2.6.3 (2019-09-04) -## New Features +### New Features -### Custom Scripts ([#3415](https://github.com/netbox-community/netbox/issues/3415)) +#### Custom Scripts ([#3415](https://github.com/netbox-community/netbox/issues/3415)) Custom scripts allow for the execution of arbitrary code via the NetBox UI. They can be used to automatically create, manipulate, or clean up objects or perform other tasks within NetBox. Scripts are defined as Python files which contain one or more subclasses of `extras.scripts.Script`. Variable fields can be defined within scripts, which render as form fields within the web UI to prompt the user for input data. Scripts are executed and information is logged via the web UI. Please see [the docs](https://netbox.readthedocs.io/en/stable/additional-features/custom-scripts/) for more detail. Note: There are currently no API endpoints for this feature. These are planned for the upcoming v2.7 release. -## Enhancements +### Enhancements * [#3386](https://github.com/netbox-community/netbox/issues/3386) - Add `mac_address` filter for virtual machines * [#3391](https://github.com/netbox-community/netbox/issues/3391) - Update Bootstrap CSS to v3.4.1 @@ -230,7 +232,7 @@ Note: There are currently no API endpoints for this feature. These are planned f * [#3454](https://github.com/netbox-community/netbox/issues/3454) - Enable filtering circuits by region * [#3456](https://github.com/netbox-community/netbox/issues/3456) - Enable bulk editing of tag color -## Bug Fixes +### Bug Fixes * [#3392](https://github.com/netbox-community/netbox/issues/3392) - Add database index for ObjectChange time * [#3420](https://github.com/netbox-community/netbox/issues/3420) - Serial number filter for racks, devices, and inventory items is now case-insensitive @@ -241,9 +243,9 @@ Note: There are currently no API endpoints for this feature. These are planned f --- -# v2.6.2 (2019-08-02) +## v2.6.2 (2019-08-02) -## Enhancements +### Enhancements * [#984](https://github.com/netbox-community/netbox/issues/984) - Allow ordering circuits by A/Z side * [#3307](https://github.com/netbox-community/netbox/issues/3307) - Add power panels count to home page @@ -252,7 +254,7 @@ Note: There are currently no API endpoints for this feature. These are planned f * [#3368](https://github.com/netbox-community/netbox/issues/3368) - Indicate indefinite changelog retention when applicable * [#3370](https://github.com/netbox-community/netbox/issues/3370) - Add filter class to VirtualChassis API -## Bug Fixes +### Bug Fixes * [#3018](https://github.com/netbox-community/netbox/issues/3018) - Components connected via a cable must have an equal number of positions * [#3289](https://github.com/netbox-community/netbox/issues/3289) - Prevent position from being nullified when moving a device to a new rack @@ -266,15 +268,15 @@ Note: There are currently no API endpoints for this feature. These are planned f --- -# v2.6.1 (2019-06-25) +## v2.6.1 (2019-06-25) -## Enhancements +### Enhancements * [#3154](https://github.com/netbox-community/netbox/issues/3154) - Add `virtual_chassis_member` device filter * [#3277](https://github.com/netbox-community/netbox/issues/3277) - Add cable trace buttons for console and power ports * [#3281](https://github.com/netbox-community/netbox/issues/3281) - Hide custom links which render as empty text -## Bug Fixes +### Bug Fixes * [#3229](https://github.com/netbox-community/netbox/issues/3229) - Limit rack group selection by parent site on racks list * [#3269](https://github.com/netbox-community/netbox/issues/3269) - Raise validation error when specifying non-existent cable terminations @@ -286,11 +288,11 @@ Note: There are currently no API endpoints for this feature. These are planned f --- -# v2.6.0 (2019-06-20) +## v2.6.0 (2019-06-20) -## New Features +### New Features -### Power Panels and Feeds ([#54](https://github.com/netbox-community/netbox/issues/54)) +#### Power Panels and Feeds ([#54](https://github.com/netbox-community/netbox/issues/54)) NetBox now supports power circuit modeling via two new models: power panels and power feeds. Power feeds are terminated to power panels and are optionally associated with individual racks. Each power feed defines a supply type (AC/DC), @@ -301,7 +303,7 @@ Additionally, the power port model, which represents a device's power input, has denoting maximum and allocated draw, in volt-amperes. This allows a device (e.g. a PDU) to calculate its total load compared to its connected power feed. -### Caching ([#2647](https://github.com/netbox-community/netbox/issues/2647)) +#### Caching ([#2647](https://github.com/netbox-community/netbox/issues/2647)) To improve performance, NetBox now supports caching for most object and list views. Caching is implemented using Redis, which is now a required dependency. (Previously, Redis was required only if webhooks were enabled.) @@ -309,11 +311,11 @@ which is now a required dependency. (Previously, Redis was required only if webh A new configuration parameter is available to control the cache timeout: ``` -# Cache timeout (in seconds) +## Cache timeout (in seconds) CACHE_TIMEOUT = 900 ``` -### View Permissions ([#323](https://github.com/netbox-community/netbox/issues/323)) +#### View Permissions ([#323](https://github.com/netbox-community/netbox/issues/323)) Django 2.1 introduced the ability to enforce view-only permissions for different object types. NetBox now enforces these by default. You can grant view permission to a user or group by assigning the "can view" permission for the @@ -335,7 +337,7 @@ To exclude _all_ objects, effectively disabling view permissions and restoring p EXEMPT_VIEW_PERMISSIONS = ['*'] ``` -### Custom Links ([#969](https://github.com/netbox-community/netbox/issues/969)) +#### Custom Links ([#969](https://github.com/netbox-community/netbox/issues/969)) Custom links are created under the admin UI and will be displayed on each object of the selected type. Link text and URLs can be formed from Jinja2 template code, with the viewed object passed as context data. For example, to link to an @@ -348,7 +350,7 @@ https://nms.example.com/nodes/?name={{ obj.name }} Custom links appear as buttons at the top of the object view. Grouped links will render as a dropdown menu beneath a single button. -### Prometheus Metrics ([#3104](https://github.com/netbox-community/netbox/issues/3104)) +#### Prometheus Metrics ([#3104](https://github.com/netbox-community/netbox/issues/3104)) NetBox now supports exposing native Prometheus metrics from the application. [Prometheus](https://prometheus.io/) is a popular time series metric platform used for monitoring. Metric exposition can be toggled with the `METRICS_ENABLED` @@ -372,9 +374,9 @@ different types of metrics, including: For the exhaustive list of exposed metrics, visit the `/metrics` endpoint on your NetBox instance. See the documentation for more details on using Prometheus metrics in NetBox. -## Changes +### Changes -### New Dependency: Redis +#### New Dependency: 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 now required to support NetBox's new caching @@ -401,7 +403,7 @@ an additional `CACHE_DATABASE` setting has been added with a default value of 1 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 during cache flushing events. -### API Support for Specifying Related Objects by Attributes([#3077](https://github.com/netbox-community/netbox/issues/3077)) +#### API Support for Specifying Related Objects by Attributes([#3077](https://github.com/netbox-community/netbox/issues/3077)) Previously, specifying a related object in an API request required knowing the primary key (integer ID) of that object. For example, when creating a new device, its rack would be specified as an integer: @@ -433,7 +435,7 @@ rack can be identified by its name and parent site: There is no limit to the depth of nested references. Note that if the provided parameters do not return exactly one object, a validation error is raised. -### API Device/VM Config Context Included by Default ([#2350](https://github.com/netbox-community/netbox/issues/2350)) +#### API Device/VM Config Context Included by Default ([#2350](https://github.com/netbox-community/netbox/issues/2350)) The rendered config context for devices and VMs is now included by default in all API results (list and detail views). Previously, the rendered config context was available only in the detail view for individual objects. Users with large @@ -441,19 +443,19 @@ amounts of context data may observe a performance drop when returning multiple o the rendered config context is not needed, the query parameter `?exclude=config_context` may be appended to the request URL to exclude the config context data from the API response. -### Changes to Tag Permissions +#### Changes to Tag Permissions NetBox now makes use of its own `Tag` model instead of the stock model which ships with django-taggit. This new model lives in the `extras` app and thus any permissions that you may have configured using "Taggit | Tag" should be changed to now use "Extras | Tag." Also note that the admin interface for tags has been removed as it was redundant to the functionality provided by the front end UI. -### CORS_ORIGIN_WHITELIST Requires URI Scheme +#### CORS_ORIGIN_WHITELIST Requires URI Scheme If you have the `CORS_ORIGIN_WHITELIST` configuration parameter defined, note that each origin must now incldue a URI scheme. This change was introuced in django-cors-headers 3.0. -## Enhancements +### Enhancements * [#166](https://github.com/netbox-community/netbox/issues/166) - Add `dns_name` field to IPAddress * [#524](https://github.com/netbox-community/netbox/issues/524) - Added power utilization graphs to power feeds, devices, and racks @@ -467,14 +469,14 @@ scheme. This change was introuced in django-cors-headers 3.0. * [#3038](https://github.com/netbox-community/netbox/issues/3038) - OR logic now used when multiple values of a query filter are passed * [#3264](https://github.com/netbox-community/netbox/issues/3264) - Annotate changelog retention time on UI -## Bug Fixes +### Bug Fixes * [#2968](https://github.com/netbox-community/netbox/issues/2968) - Correct API documentation for SerializerMethodFields * [#3176](https://github.com/netbox-community/netbox/issues/3176) - Add cable trace button for console server ports and power outlets * [#3231](https://github.com/netbox-community/netbox/issues/3231) - Fixed cosmetic error indicating a missing schema migration * [#3239](https://github.com/netbox-community/netbox/issues/3239) - Corrected count of tags reported via API -## Bug Fixes From v2.6-beta1 +### Bug Fixes From v2.6-beta1 * [#3123](https://github.com/netbox-community/netbox/issues/3123) - Exempt `/metrics` view from authentication * [#3125](https://github.com/netbox-community/netbox/issues/3125) - Fix exception when viewing PDUs @@ -487,7 +489,7 @@ scheme. This change was introuced in django-cors-headers 3.0. * [#3207](https://github.com/netbox-community/netbox/issues/3207) - Fix link for connecting interface to rear port * [#3258](https://github.com/netbox-community/netbox/issues/3258) - Exception raised when creating/viewing a circuit with a non-connected termination -## API Changes +### API Changes * New API endpoints for power modeling: `/api/dcim/power-panels/` and `/api/dcim/power-feeds/` * New API endpoint for custom field choices: `/api/extras/_custom_field_choices/` diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index a7ff3a182..69be137d7 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -1,24 +1,94 @@ -# v2.7.8 (FUTURE) +# NetBox v2.7 Release Notes -## Bug Fixes +## v2.7.10 (2020-03-10) -* [#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 -* [#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 +**Note:** If your deployment requires any non-core Python packages (such as `napalm`, `django-storages`, or `django-auth-ldap`), list them in a file named `local_requirements.txt` in the NetBox root directory (alongside `requirements.txt`). This will ensure they are detected and re-installed by the upgrade script when the Python virtual environment is rebuilt. + +### Enhancements + +* [#4217](https://github.com/netbox-community/netbox/issues/4217) - Embed model documentation within web UI +* [#4323](https://github.com/netbox-community/netbox/issues/4323) - Add bulk edit view for power panels +* [#4324](https://github.com/netbox-community/netbox/issues/4324) - Add CSV import view for services +* [#4325](https://github.com/netbox-community/netbox/issues/4324) - Add CSV import view for rack reservations +* [#4332](https://github.com/netbox-community/netbox/issues/4332) - Redirect to a user-friendly error page when CSS/JS resources fail to load + +### Bug Fixes + +* [#4326](https://github.com/netbox-community/netbox/issues/4326) - Exclude Python modules without Script classes from scripts list +* [#4337](https://github.com/netbox-community/netbox/issues/4337) - Allow bulk editing/deletion of all device components matching a query +* [#4338](https://github.com/netbox-community/netbox/issues/4338) - Catch `AddrFormatError` exception when filtering aggregates/prefixes by an invalid prefix --- -# v2.7.7 (2020-02-20) +## v2.7.9 (2020-03-06) + +**Note:** This release will deploy a Python virtual environment on upgrade in the `venv/` directory. This will require modifying the paths to your Python and gunicorn executables in the systemd service files. For more detail, please see the [upgrade instructions](https://netbox.readthedocs.io/en/stable/installation/upgrading/). + +### Enhancements + +* [#3949](https://github.com/netbox-community/netbox/issues/3949) - Revised the installation docs and upgrade script to employ a Python virtual environment +* [#4062](https://github.com/netbox-community/netbox/issues/4062) - Enumerate ChoiceField type and value in API +* [#4119](https://github.com/netbox-community/netbox/issues/4119) - Extend upgrade script to clear expired user sessions +* [#4121](https://github.com/netbox-community/netbox/issues/4121) - Add dynamic lookup expressions for all filters +* [#4218](https://github.com/netbox-community/netbox/issues/4218) - Allow negative voltage for DC power feeds +* [#4281](https://github.com/netbox-community/netbox/issues/4281) - Allow filtering device component list views by type +* [#4284](https://github.com/netbox-community/netbox/issues/4284) - Add MRJ21 port and cable types +* [#4290](https://github.com/netbox-community/netbox/issues/4290) - Include device name in tooltip on rack elevations +* [#4305](https://github.com/netbox-community/netbox/issues/4305) - Add 10-inch option for rack width + +### Bug Fixes + +* [#4274](https://github.com/netbox-community/netbox/issues/4274) - Fix incorrect schema definition of `int` type choicefields +* [#4277](https://github.com/netbox-community/netbox/issues/4277) - Fix filtering of clusters by tenant +* [#4282](https://github.com/netbox-community/netbox/issues/4282) - Fix label on export button for device types +* [#4285](https://github.com/netbox-community/netbox/issues/4285) - Include A/Z termination sites in provider circuits table +* [#4295](https://github.com/netbox-community/netbox/issues/4295) - Fix assignment of parent LAG during interface bulk edit +* [#4298](https://github.com/netbox-community/netbox/issues/4298) - Fix bulk creation of objects with custom fields via REST API +* [#4300](https://github.com/netbox-community/netbox/issues/4300) - Pass "commit" argument when executing scripts via REST API +* [#4301](https://github.com/netbox-community/netbox/issues/4301) - Fix exception when deleting device type with components +* [#4306](https://github.com/netbox-community/netbox/issues/4306) - Fix toggling of device images for all racks in elevations view + +--- + +## 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 +### 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 @@ -28,7 +98,7 @@ python3 manage.py renaturalize dcim.Interface * [#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 +### 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" @@ -45,19 +115,19 @@ python3 manage.py renaturalize dcim.Interface --- -# v2.7.6 (2020-02-13) +## v2.7.6 (2020-02-13) -## Bug Fixes +### 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) +## 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 +### 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 @@ -69,7 +139,7 @@ python3 manage.py renaturalize dcim.Interface * [#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 +### 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 @@ -88,9 +158,9 @@ python3 manage.py renaturalize dcim.Interface --- -# v2.7.4 (2020-02-04) +## v2.7.4 (2020-02-04) -## Enhancements +### 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 @@ -98,7 +168,7 @@ python3 manage.py renaturalize dcim.Interface * [#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 +### 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 @@ -112,9 +182,9 @@ python3 manage.py renaturalize dcim.Interface --- -# v2.7.3 (2020-01-28) +## v2.7.3 (2020-01-28) -## Enhancements +### 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 @@ -122,7 +192,7 @@ python3 manage.py renaturalize dcim.Interface * [#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 +### 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 @@ -141,15 +211,15 @@ python3 manage.py renaturalize dcim.Interface --- -# v2.7.2 (2020-01-21) +## v2.7.2 (2020-01-21) -## Enhancements +### 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 +### 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 @@ -165,9 +235,9 @@ python3 manage.py renaturalize dcim.Interface --- -# v2.7.1 (2020-01-16) +## v2.7.1 (2020-01-16) -## Bug Fixes +### 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 @@ -175,16 +245,16 @@ python3 manage.py renaturalize dcim.Interface --- -# v2.7.0 (2020-01-16) +## 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 +### New Features -### Enhanced Device Type Import ([#451](https://github.com/netbox-community/netbox/issues/451)) +#### 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 @@ -214,7 +284,7 @@ console-ports: 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)) +#### 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: @@ -229,7 +299,7 @@ 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)) +#### 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 @@ -265,7 +335,7 @@ STORAGE_CONFIG = { 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)) +#### 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 @@ -294,20 +364,20 @@ request either the `front` or `rear` of the elevation. Below is in example reque Thanks to [@hellerve](https://github.com/hellerve) for doing the heavy lifting on this! -## Changes +### Changes -### Topology Maps Removed ([#2745](https://github.com/netbox-community/netbox/issues/2745)) +#### 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)) +#### 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)) +#### 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 @@ -355,14 +425,14 @@ Note that the `CACHE_DATABASE` parameter has been removed and the connection set `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)) +#### 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)) +#### 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 @@ -394,7 +464,7 @@ Note that that all v2.7 releases will continue to accept the legacy integer valu `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 +### 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 @@ -417,7 +487,7 @@ for convenient reference as consumers adopt to the new string values. This behav * [#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 +### 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 @@ -429,12 +499,12 @@ for convenient reference as consumers adopt to the new string values. This behav * [#3930](https://github.com/netbox-community/netbox/issues/3930) - Fix API rendering of the `family` field for aggregates -## Bug Fixes (From Beta) +### 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 +### 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)). diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md index 3088bb356..38cda9414 100644 --- a/docs/release-notes/version-2.8.md +++ b/docs/release-notes/version-2.8.md @@ -1,8 +1,13 @@ # v2.8.0 (FUTURE) +## Enhancements + +* [#4195](https://github.com/netbox-community/netbox/issues/4195) - Enabled application logging (see [logging configuration](../configuration/optional-settings.md#logging)) + ## API Changes * dcim.Rack: The `/api/dcim/racks//units/` endpoint has been replaced with `/api/dcim/racks//elevation/`. +* The `id__in` filter has been removed. Use the format `?id=1&id=2` instead. ([#4313](https://github.com/netbox-community/netbox/issues/4313)) ## Other Changes diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..9c863fec7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +mkdocs==1.1 +git+https://github.com/cmacmackin/markdown-include.git diff --git a/mkdocs.yml b/mkdocs.yml index 4ba91dfe5..d980cc80c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,17 +1,27 @@ -site_name: NetBox -theme: readthedocs +site_name: NetBox Documentation +site_url: https://netbox.readthedocs.io/ repo_url: https://github.com/netbox-community/netbox +python: + install: + - requirements: docs/requirements.txt +theme: + name: readthedocs + navigation_depth: 3 +markdown_extensions: + - admonition: + - markdown_include.include: + headingOffset: 1 -pages: +nav: - Introduction: 'index.md' - Installation: - Installing NetBox: 'installation/index.md' - 1. PostgreSQL: 'installation/1-postgresql.md' - - 2. NetBox: 'installation/2-netbox.md' - - 3. HTTP Daemon: 'installation/3-http-daemon.md' - - 4. LDAP (Optional): 'installation/4-ldap.md' + - 2. Redis: 'installation/2-redis.md' + - 3. NetBox: 'installation/3-netbox.md' + - 4. HTTP Daemon: 'installation/4-http-daemon.md' + - 5. LDAP (Optional): 'installation/5-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' @@ -19,15 +29,16 @@ pages: - Optional Settings: 'configuration/optional-settings.md' - Core Functionality: - IP Address Management: 'core-functionality/ipam.md' - - VLANs: 'core-functionality/vlans.md' + - VLAN Management: 'core-functionality/vlans.md' - Sites and Racks: 'core-functionality/sites-and-racks.md' - - Devices: 'core-functionality/devices.md' - - Virtual Machines: 'core-functionality/virtual-machines.md' - - Services: 'core-functionality/services.md' + - Devices and Cabling: 'core-functionality/devices.md' + - Device Types: 'core-functionality/device-types.md' + - Virtualization: 'core-functionality/virtualization.md' + - Service Mapping: 'core-functionality/services.md' - Circuits: 'core-functionality/circuits.md' - - Power: 'core-functionality/power.md' + - Power Tracking: 'core-functionality/power.md' - Secrets: 'core-functionality/secrets.md' - - Tenancy: 'core-functionality/tenancy.md' + - Tenancy Assignment: 'core-functionality/tenancy.md' - Additional Features: - Caching: 'additional-features/caching.md' - Change Logging: 'additional-features/change-logging.md' @@ -47,6 +58,7 @@ pages: - NetBox Shell: 'administration/netbox-shell.md' - API: - Overview: 'api/overview.md' + - Filtering: 'api/filtering.md' - Authentication: 'api/authentication.md' - Working with Secrets: 'api/working-with-secrets.md' - Examples: 'api/examples.md' @@ -58,6 +70,7 @@ pages: - Release Checklist: 'development/release-checklist.md' - Squashing Migrations: 'development/squashing-migrations.md' - Release Notes: + - Version 2.8: 'release-notes/version-2.8.md' - 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' @@ -66,16 +79,3 @@ pages: - Version 2.2: 'release-notes/version-2.2.md' - Version 2.1: 'release-notes/version-2.1.md' - Version 2.0: 'release-notes/version-2.0.md' - - Version 1.9: 'release-notes/version-1.9.md' - - Version 1.8: 'release-notes/version-1.8.md' - - Version 1.7: 'release-notes/version-1.7.md' - - Version 1.6: 'release-notes/version-1.6.md' - - Version 1.5: 'release-notes/version-1.5.md' - - Version 1.4: 'release-notes/version-1.4.md' - - Version 1.3: 'release-notes/version-1.3.md' - - Version 1.2: 'release-notes/version-1.2.md' - - Version 1.1: 'release-notes/version-1.1.md' - - Version 1.0: 'release-notes/version-1.0.md' - -markdown_extensions: - - admonition: diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index c27ffb8d7..b8d97d77d 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -4,7 +4,9 @@ from django.db.models import Q from dcim.models import Region, Site from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from tenancy.filters import TenancyFilterSet -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter +from utilities.filters import ( + BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter +) from .choices import * from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -16,23 +18,21 @@ __all__ = ( ) -class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='circuits__terminations__site__region__in', + field_name='circuits__terminations__site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='circuits__terminations__site__region__in', + field_name='circuits__terminations__site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -65,18 +65,14 @@ class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): ) -class CircuitTypeFilterSet(NameSlugSearchFilterSet): +class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = CircuitType fields = ['id', 'name', 'slug'] -class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -118,12 +114,14 @@ class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFil ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='terminations__site__region__in', + field_name='terminations__site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='terminations__site__region__in', + field_name='terminations__site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -146,7 +144,7 @@ class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFil ).distinct() -class CircuitTerminationFilterSet(django_filters.FilterSet): +class CircuitTerminationFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 59f6e2004..812eaa79e 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -10,6 +10,7 @@ from extras.models import CustomFieldModel, ObjectChange, TaggedItem from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object from .choices import * +from .querysets import CircuitQuerySet __all__ = ( @@ -184,6 +185,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) + objects = CircuitQuerySet.as_manager() tags = TaggableManager(through=TaggedItem) csv_headers = [ diff --git a/netbox/circuits/querysets.py b/netbox/circuits/querysets.py new file mode 100644 index 000000000..60956f32a --- /dev/null +++ b/netbox/circuits/querysets.py @@ -0,0 +1,15 @@ +from django.db.models import OuterRef, QuerySet, Subquery + + +class CircuitQuerySet(QuerySet): + + def annotate_sites(self): + """ + Annotate the A and Z termination site names for ordering. + """ + from circuits.models import CircuitTermination + _terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk')) + return self.annotate( + a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]), + z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]), + ) diff --git a/netbox/circuits/tests/test_filters.py b/netbox/circuits/tests/test_filters.py index 46c2bacbe..8ce679501 100644 --- a/netbox/circuits/tests/test_filters.py +++ b/netbox/circuits/tests/test_filters.py @@ -4,6 +4,7 @@ from circuits.choices import * from circuits.filters import * from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from dcim.models import Region, Site +from tenancy.models import Tenant, TenantGroup class ProviderTestCase(TestCase): @@ -69,11 +70,6 @@ class ProviderTestCase(TestCase): 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]} @@ -138,6 +134,20 @@ class CircuitTestCase(TestCase): ) Site.objects.bulk_create(sites) + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + TenantGroup.objects.bulk_create(tenant_groups) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + circuit_types = ( CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'), CircuitType(name='Test Circuit Type 2', slug='test-circuit-type-2'), @@ -151,12 +161,12 @@ class CircuitTestCase(TestCase): 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(provider=providers[0], tenant=tenants[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], tenant=tenants[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], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED), + Circuit(provider=providers[1], tenant=tenants[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], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE), + Circuit(provider=providers[1], tenant=tenants[2], 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) @@ -179,11 +189,6 @@ class CircuitTestCase(TestCase): 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]} @@ -216,6 +221,20 @@ class CircuitTestCase(TestCase): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + class CircuitTerminationTestCase(TestCase): queryset = CircuitTermination.objects.all() diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index ba873f23f..b092e1855 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -37,10 +37,14 @@ class ProviderView(PermissionRequiredMixin, View): def get(self, request, slug): provider = get_object_or_404(Provider, slug=slug) - circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site') + circuits = Circuit.objects.filter( + provider=provider + ).prefetch_related( + 'type', 'tenant', 'terminations__site' + ).annotate_sites() show_graphs = Graph.objects.filter(type__model='provider').exists() - circuits_table = tables.CircuitTable(circuits, orderable=False) + circuits_table = tables.CircuitTable(circuits) circuits_table.columns.hide('provider') paginate = { @@ -142,10 +146,7 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView): _terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk')) queryset = Circuit.objects.prefetch_related( 'provider', 'type', 'tenant', 'terminations__site' - ).annotate( - a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]), - z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]), - ) + ).annotate_sites() filterset = filters.CircuitFilterSet filterset_form = forms.CircuitFilterForm table = tables.CircuitTable diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 30d84f703..bb2d61faa 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -3,8 +3,8 @@ from rest_framework import serializers from dcim.constants import CONNECTION_STATUS_CHOICES from dcim.models import ( Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, - Interface, Manufacturer, Platform, 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 +25,7 @@ __all__ = [ 'NestedPowerOutletSerializer', 'NestedPowerPanelSerializer', 'NestedPowerPortSerializer', + 'NestedPowerPortTemplateSerializer', 'NestedRackGroupSerializer', 'NestedRackRoleSerializer', 'NestedRackSerializer', @@ -111,6 +112,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') diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 34d9a7890..5483904f5 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -172,6 +172,10 @@ class RackReservationSerializer(ValidatedModelSerializer): class RackElevationDetailFilterSerializer(serializers.Serializer): + q = serializers.CharField( + required=False, + default=None + ) face = serializers.ChoiceField( choices=DeviceFaceChoices, default=DeviceFaceChoices.FACE_FRONT @@ -278,7 +282,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer): allow_blank=True, required=False ) - power_port = PowerPortTemplateSerializer( + power_port = NestedPowerPortTemplateSerializer( required=False ) feed_leg = ChoiceField( @@ -294,7 +298,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer): class InterfaceTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() - type = ChoiceField(choices=InterfaceTypeChoices, required=False) + type = ChoiceField(choices=InterfaceTypeChoices) class Meta: model = InterfaceTemplate @@ -514,7 +518,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() - type = ChoiceField(choices=InterfaceTypeChoices, required=False) + type = ChoiceField(choices=InterfaceTypeChoices) lag = NestedInterfaceSerializer(required=False, allow_null=True) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 9b0d15ad9..d044d6198 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -210,6 +210,11 @@ class RackViewSet(CustomFieldModelViewSet): 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}) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index e5b77dbaf..79f00bce4 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -55,10 +55,12 @@ class RackTypeChoices(ChoiceSet): class RackWidthChoices(ChoiceSet): + WIDTH_10IN = 10 WIDTH_19IN = 19 WIDTH_23IN = 23 CHOICES = ( + (WIDTH_10IN, '10 inches'), (WIDTH_19IN, '19 inches'), (WIDTH_23IN, '23 inches'), ) @@ -836,6 +838,7 @@ class PortTypeChoices(ChoiceSet): TYPE_8P8C = '8p8c' TYPE_110_PUNCH = '110-punch' TYPE_BNC = 'bnc' + TYPE_MRJ21 = 'mrj21' TYPE_ST = 'st' TYPE_SC = 'sc' TYPE_SC_APC = 'sc-apc' @@ -854,6 +857,7 @@ class PortTypeChoices(ChoiceSet): (TYPE_8P8C, '8P8C'), (TYPE_110_PUNCH, '110 Punch'), (TYPE_BNC, 'BNC'), + (TYPE_MRJ21, 'MRJ21'), ), ), ( @@ -904,6 +908,7 @@ class CableTypeChoices(ChoiceSet): TYPE_CAT7 = 'cat7' TYPE_DAC_ACTIVE = 'dac-active' TYPE_DAC_PASSIVE = 'dac-passive' + TYPE_MRJ21_TRUNK = 'mrj21-trunk' TYPE_COAXIAL = 'coaxial' TYPE_MMF = 'mmf' TYPE_MMF_OM1 = 'mmf-om1' @@ -927,6 +932,7 @@ class CableTypeChoices(ChoiceSet): (TYPE_CAT7, 'CAT7'), (TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'), (TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'), + (TYPE_MRJ21_TRUNK, 'MRJ21 Trunk'), (TYPE_COAXIAL, 'Coaxial'), ), ), @@ -973,10 +979,12 @@ class CableStatusChoices(ChoiceSet): STATUS_CONNECTED = 'connected' STATUS_PLANNED = 'planned' + STATUS_DECOMMISSIONING = 'decommissioning' CHOICES = ( (STATUS_CONNECTED, 'Connected'), (STATUS_PLANNED, 'Planned'), + (STATUS_DECOMMISSIONING, 'Decommissioning'), ) LEGACY_MAP = { diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 39f30639e..78a418283 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -61,13 +61,10 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage # Cabling and connections # -# TODO: Replace with CableStatusChoices? # 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 diff --git a/netbox/dcim/elevations.py b/netbox/dcim/elevations.py index a3672f60d..a1af3968c 100644 --- a/netbox/dcim/elevations.py +++ b/netbox/dcim/elevations.py @@ -20,6 +20,16 @@ class RackElevationSVG: self.rack = rack self.include_images = include_images + def _get_device_description(self, device): + return '{} ({}) — {} ({}U) {} {}'.format( + device.name, + device.device_role, + device.device_type.display_name, + device.device_type.u_height, + device.asset_tag or '', + device.serial or '' + ) + @staticmethod def _add_gradient(drawing, id_, color): gradient = drawing.linearGradient( @@ -64,10 +74,7 @@ class RackElevationSVG: 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.set_desc(self._get_device_description(device)) 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)) @@ -81,10 +88,7 @@ class RackElevationSVG: 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 '' - )) + rect.set_desc(self._get_device_description(device)) drawing.add(rect) drawing.add(drawing.text(str(device), insert=text)) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 7b278ca0e..a268e3114 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -6,8 +6,8 @@ from tenancy.filters import TenancyFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES from utilities.filters import ( - MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, - TagFilter, TreeNodeMultipleChoiceFilter, + BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, + NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter, ) from virtualization.models import Cluster from .choices import * @@ -60,7 +60,7 @@ __all__ = ( ) -class RegionFilterSet(NameSlugSearchFilterSet): +class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=Region.objects.all(), label='Parent region (ID)', @@ -77,11 +77,7 @@ class RegionFilterSet(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -92,12 +88,14 @@ class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='region__in', + field_name='region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='region__in', + field_name='region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -131,15 +129,17 @@ class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter return queryset.filter(qs_filter) -class RackGroupFilterSet(NameSlugSearchFilterSet): +class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -159,30 +159,28 @@ class RackGroupFilterSet(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class RackRoleFilterSet(NameSlugSearchFilterSet): +class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = RackRole fields = ['id', 'name', 'slug', 'color'] -class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -244,11 +242,7 @@ class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter ) -class RackReservationFilterSet(TenancyFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -305,18 +299,14 @@ class RackReservationFilterSet(TenancyFilterSet): ) -class ManufacturerFilterSet(NameSlugSearchFilterSet): +class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = Manufacturer fields = ['id', 'name', 'slug'] -class DeviceTypeFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -410,70 +400,70 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet): ) -class ConsolePortTemplateFilterSet(DeviceTypeComponentFilterSet): +class ConsolePortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class Meta: model = ConsolePortTemplate fields = ['id', 'name', 'type'] -class ConsoleServerPortTemplateFilterSet(DeviceTypeComponentFilterSet): +class ConsoleServerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class Meta: model = ConsoleServerPortTemplate fields = ['id', 'name', 'type'] -class PowerPortTemplateFilterSet(DeviceTypeComponentFilterSet): +class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class Meta: model = PowerPortTemplate fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw'] -class PowerOutletTemplateFilterSet(DeviceTypeComponentFilterSet): +class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class Meta: model = PowerOutletTemplate fields = ['id', 'name', 'type', 'feed_leg'] -class InterfaceTemplateFilterSet(DeviceTypeComponentFilterSet): +class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class Meta: model = InterfaceTemplate fields = ['id', 'name', 'type', 'mgmt_only'] -class FrontPortTemplateFilterSet(DeviceTypeComponentFilterSet): +class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class Meta: model = FrontPortTemplate fields = ['id', 'name', 'type'] -class RearPortTemplateFilterSet(DeviceTypeComponentFilterSet): +class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class Meta: model = RearPortTemplate fields = ['id', 'name', 'type', 'positions'] -class DeviceBayTemplateFilterSet(DeviceTypeComponentFilterSet): +class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class Meta: model = DeviceBayTemplate fields = ['id', 'name'] -class DeviceRoleFilterSet(NameSlugSearchFilterSet): +class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = DeviceRole fields = ['id', 'name', 'slug', 'color', 'vm_role'] -class PlatformFilterSet(NameSlugSearchFilterSet): +class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet): manufacturer_id = django_filters.ModelMultipleChoiceFilter( field_name='manufacturer', queryset=Manufacturer.objects.all(), @@ -491,11 +481,13 @@ class PlatformFilterSet(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug', 'napalm_driver'] -class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class DeviceFilterSet( + BaseFilterSet, + TenancyFilterSet, + LocalConfigContextFilterSet, + CustomFieldFilterSet, + CreatedUpdatedFilterSet +): q = django_filters.CharFilter( method='search', label='Search', @@ -538,12 +530,14 @@ class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomField ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -697,12 +691,14 @@ class DeviceComponentFilterSet(django_filters.FilterSet): ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='device__site__region__in', + field_name='device__site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='device__site__region__in', + field_name='device__site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -738,7 +734,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet): ) -class ConsolePortFilterSet(DeviceComponentFilterSet): +class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet): type = django_filters.MultipleChoiceFilter( choices=ConsolePortTypeChoices, null_value=None @@ -754,7 +750,7 @@ class ConsolePortFilterSet(DeviceComponentFilterSet): fields = ['id', 'name', 'description', 'connection_status'] -class ConsoleServerPortFilterSet(DeviceComponentFilterSet): +class ConsoleServerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): type = django_filters.MultipleChoiceFilter( choices=ConsolePortTypeChoices, null_value=None @@ -770,7 +766,7 @@ class ConsoleServerPortFilterSet(DeviceComponentFilterSet): fields = ['id', 'name', 'description', 'connection_status'] -class PowerPortFilterSet(DeviceComponentFilterSet): +class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerPortTypeChoices, null_value=None @@ -786,7 +782,7 @@ class PowerPortFilterSet(DeviceComponentFilterSet): fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status'] -class PowerOutletFilterSet(DeviceComponentFilterSet): +class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerOutletTypeChoices, null_value=None @@ -802,7 +798,7 @@ class PowerOutletFilterSet(DeviceComponentFilterSet): fields = ['id', 'name', 'feed_leg', 'description', 'connection_status'] -class InterfaceFilterSet(DeviceComponentFilterSet): +class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -900,7 +896,7 @@ class InterfaceFilterSet(DeviceComponentFilterSet): }.get(value, queryset.none()) -class FrontPortFilterSet(DeviceComponentFilterSet): +class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -912,7 +908,7 @@ class FrontPortFilterSet(DeviceComponentFilterSet): fields = ['id', 'name', 'type', 'description'] -class RearPortFilterSet(DeviceComponentFilterSet): +class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -924,26 +920,28 @@ class RearPortFilterSet(DeviceComponentFilterSet): fields = ['id', 'name', 'type', 'positions', 'description'] -class DeviceBayFilterSet(DeviceComponentFilterSet): +class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet): class Meta: model = DeviceBay fields = ['id', 'name', 'description'] -class InventoryItemFilterSet(DeviceComponentFilterSet): +class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): q = django_filters.CharFilter( method='search', label='Search', ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='device__site__region__in', + field_name='device__site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='device__site__region__in', + field_name='device__site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -1002,19 +1000,21 @@ class InventoryItemFilterSet(DeviceComponentFilterSet): return queryset.filter(qs_filter) -class VirtualChassisFilterSet(django_filters.FilterSet): +class VirtualChassisFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', label='Search', ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='master__site__region__in', + field_name='master__site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='master__site__region__in', + field_name='master__site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -1056,7 +1056,7 @@ class VirtualChassisFilterSet(django_filters.FilterSet): return queryset.filter(qs_filter) -class CableFilterSet(django_filters.FilterSet): +class CableFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -1119,7 +1119,7 @@ class CableFilterSet(django_filters.FilterSet): return queryset -class ConsoleConnectionFilterSet(django_filters.FilterSet): +class ConsoleConnectionFilterSet(BaseFilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1150,7 +1150,7 @@ class ConsoleConnectionFilterSet(django_filters.FilterSet): ) -class PowerConnectionFilterSet(django_filters.FilterSet): +class PowerConnectionFilterSet(BaseFilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1181,7 +1181,7 @@ class PowerConnectionFilterSet(django_filters.FilterSet): ) -class InterfaceConnectionFilterSet(django_filters.FilterSet): +class InterfaceConnectionFilterSet(BaseFilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1215,23 +1215,21 @@ class InterfaceConnectionFilterSet(django_filters.FilterSet): ) -class PowerPanelFilterSet(django_filters.FilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class PowerPanelFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', label='Search', ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -1264,23 +1262,21 @@ class PowerPanelFilterSet(django_filters.FilterSet): return queryset.filter(qs_filter) -class PowerFeedFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='power_panel__site__region__in', + field_name='power_panel__site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='power_panel__site__region__in', + field_name='power_panel__site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 2b6ba18da..88d72eeda 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -829,6 +829,64 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): return unit_choices +class RackReservationCSVForm(forms.ModelForm): + site = forms.ModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + help_text='Name of parent site', + error_messages={ + 'invalid_choice': 'Invalid site name.', + } + ) + rack_group = forms.CharField( + required=False, + help_text="Rack's group (if any)" + ) + rack_name = forms.CharField( + help_text="Rack name" + ) + units = SimpleArrayField( + base_field=forms.IntegerField(), + required=True, + help_text='Comma-separated list of individual unit numbers' + ) + tenant = forms.ModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Name of assigned tenant', + error_messages={ + 'invalid_choice': 'Tenant not found.', + } + ) + + class Meta: + model = RackReservation + fields = ('site', 'rack_group', 'rack_name', 'units', 'tenant', 'description') + help_texts = { + } + + def clean(self): + + super().clean() + + site = self.cleaned_data.get('site') + rack_group = self.cleaned_data.get('rack_group') + rack_name = self.cleaned_data.get('rack_name') + + # Validate rack + if site and rack_group and rack_name: + try: + self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name) + except Rack.DoesNotExist: + raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group)) + elif site and rack_name: + try: + self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name) + except Rack.DoesNotExist: + raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site)) + + class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackReservation.objects.all(), @@ -2344,6 +2402,11 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): class ConsolePortFilterForm(DeviceComponentFilterForm): model = ConsolePort + type = forms.MultipleChoiceField( + choices=ConsolePortTypeChoices, + required=False, + widget=StaticSelect2Multiple() + ) tag = TagFilterField(model) @@ -2429,6 +2492,11 @@ class ConsolePortCSVForm(forms.ModelForm): class ConsoleServerPortFilterForm(DeviceComponentFilterForm): model = ConsoleServerPort + type = forms.MultipleChoiceField( + choices=ConsolePortTypeChoices, + required=False, + widget=StaticSelect2Multiple() + ) tag = TagFilterField(model) @@ -2528,6 +2596,11 @@ class ConsoleServerPortCSVForm(forms.ModelForm): class PowerPortFilterForm(DeviceComponentFilterForm): model = PowerPort + type = forms.MultipleChoiceField( + choices=PowerPortTypeChoices, + required=False, + widget=StaticSelect2Multiple() + ) tag = TagFilterField(model) @@ -2633,6 +2706,11 @@ class PowerPortCSVForm(forms.ModelForm): class PowerOutletFilterForm(DeviceComponentFilterForm): model = PowerOutlet + type = forms.MultipleChoiceField( + choices=PowerOutletTypeChoices, + required=False, + widget=StaticSelect2Multiple() + ) tag = TagFilterField(model) @@ -2821,6 +2899,11 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): class InterfaceFilterForm(DeviceComponentFilterForm): model = Interface + type = forms.MultipleChoiceField( + choices=InterfaceTypeChoices, + required=False, + widget=StaticSelect2Multiple() + ) enabled = forms.NullBooleanField( required=False, widget=StaticSelect2( @@ -3190,6 +3273,11 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): class FrontPortFilterForm(DeviceComponentFilterForm): model = FrontPort + type = forms.MultipleChoiceField( + choices=PortTypeChoices, + required=False, + widget=StaticSelect2Multiple() + ) tag = TagFilterField(model) @@ -3379,6 +3467,11 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): class RearPortFilterForm(DeviceComponentFilterForm): model = RearPort + type = forms.MultipleChoiceField( + choices=PortTypeChoices, + required=False, + widget=StaticSelect2Multiple() + ) tag = TagFilterField(model) @@ -4586,6 +4679,35 @@ class PowerPanelCSVForm(forms.ModelForm): ) +class PowerPanelBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerPanel.objects.all(), + widget=forms.MultipleHiddenInput + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/sites/", + filter_for={ + 'rack_group': 'site_id', + } + ) + ) + rack_group = DynamicModelChoiceField( + queryset=RackGroup.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/rack-groups/" + ) + ) + + class Meta: + nullable_fields = ( + 'rack_group', + ) + + class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): model = PowerPanel q = forms.CharField( 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 deleted file mode 100644 index f74572c6f..000000000 --- a/netbox/dcim/migrations/0071_device_components_add_description_squashed_0088_powerfeed_available_power.py +++ /dev/null @@ -1,839 +0,0 @@ -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/0099_powerfeed_negative_voltage.py b/netbox/dcim/migrations/0099_powerfeed_negative_voltage.py new file mode 100644 index 000000000..db16fbc91 --- /dev/null +++ b/netbox/dcim/migrations/0099_powerfeed_negative_voltage.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.10 on 2020-03-03 16:59 + +from django.db import migrations, models +import utilities.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0098_devicetype_images'), + ] + + operations = [ + migrations.AlterField( + model_name='powerfeed', + name='voltage', + field=models.SmallIntegerField(default=120, validators=[utilities.validators.ExclusionValidator([0])]), + ), + ] diff --git a/netbox/dcim/migrations/0099_mptt_remove_indexes.py b/netbox/dcim/migrations/0100_mptt_remove_indexes.py similarity index 92% rename from netbox/dcim/migrations/0099_mptt_remove_indexes.py rename to netbox/dcim/migrations/0100_mptt_remove_indexes.py index 4b6dc0741..79d9cb597 100644 --- a/netbox/dcim/migrations/0099_mptt_remove_indexes.py +++ b/netbox/dcim/migrations/0100_mptt_remove_indexes.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('dcim', '0098_devicetype_images'), + ('dcim', '0099_powerfeed_negative_voltage'), ] operations = [ diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 5848a6201..af785f0d2 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -20,10 +20,11 @@ from dcim.choices import * from dcim.constants import * from dcim.fields import ASNField from dcim.elevations import RackElevationSVG -from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem +from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem from utilities.fields import ColorField, NaturalOrderingField from utilities.models import ChangeLoggedModel -from utilities.utils import to_meters +from utilities.utils import serialize_object, to_meters +from utilities.validators import ExclusionValidator from .device_component_templates import ( ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, @@ -118,6 +119,15 @@ class Region(MPTTModel, ChangeLoggedModel): 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 @@ -751,6 +761,8 @@ class RackReservation(ChangeLoggedModel): max_length=100 ) + csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description'] + class Meta: ordering = ['created'] @@ -783,6 +795,17 @@ class RackReservation(ChangeLoggedModel): ) }) + def to_csv(self): + return ( + self.rack.site.name, + self.rack.group if self.rack.group else None, + self.rack.name, + ','.join([str(u) for u in self.units]), + self.tenant.name if self.tenant else None, + self.user.username, + self.description + ) + @property def unit_list(self): """ @@ -1766,9 +1789,9 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): choices=PowerFeedPhaseChoices, default=PowerFeedPhaseChoices.PHASE_SINGLE ) - voltage = models.PositiveSmallIntegerField( - validators=[MinValueValidator(1)], - default=POWERFEED_VOLTAGE_DEFAULT + voltage = models.SmallIntegerField( + default=POWERFEED_VOLTAGE_DEFAULT, + validators=[ExclusionValidator([0])] ) amperage = models.PositiveSmallIntegerField( validators=[MinValueValidator(1)], @@ -1850,10 +1873,16 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): self.rack, self.rack.site, self.power_panel, self.power_panel.site )) + # AC voltage cannot be negative + if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC: + raise ValidationError({ + "voltage": "Voltage cannot be negative for AC supply" + }) + def save(self, *args, **kwargs): # Cache the available_power property on the instance - kva = self.voltage * self.amperage * (self.max_utilization / 100) + kva = abs(self.voltage) * self.amperage * (self.max_utilization / 100) if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE: self.available_power = round(kva * 1.732) else: @@ -1956,6 +1985,7 @@ class Cable(ChangeLoggedModel): STATUS_CLASS_MAP = { CableStatusChoices.STATUS_CONNECTED: 'success', CableStatusChoices.STATUS_PLANNED: 'info', + CableStatusChoices.STATUS_DECOMMISSIONING: 'warning', } class Meta: @@ -2116,14 +2146,14 @@ class Cable(ChangeLoggedModel): b_path = self.termination_a.trace() # Determine overall path status (connected or planned) - if self.status == CableStatusChoices.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 == CableStatusChoices.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] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index faa42b035..164d37d77 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -1,4 +1,4 @@ -from django.core.exceptions import ValidationError +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -37,11 +37,17 @@ class ComponentTemplateModel(models.Model): raise NotImplementedError() def to_objectchange(self, action): + # Annotate the parent DeviceType + try: + device_type = self.device_type + except ObjectDoesNotExist: + # The parent DeviceType has already been deleted + device_type = None return ObjectChange( changed_object=self, object_repr=str(self), action=action, - related_object=self.device_type, + related_object=device_type, object_data=serialize_object(self) ) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index a41eda576..9a3d608d7 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -360,9 +360,21 @@ class PowerPort(CableTermination, ComponentModel): @property def connected_endpoint(self): - if self._connected_poweroutlet: - return self._connected_poweroutlet - return self._connected_powerfeed + """ + 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): @@ -717,9 +729,21 @@ class Interface(CableTermination, ComponentModel): @property def connected_endpoint(self): - if self._connected_interface: - return self._connected_interface - return self._connected_circuittermination + """ + 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): diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 607637d1e..a0fb442f3 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -4,7 +4,6 @@ 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 ( @@ -589,6 +588,28 @@ class RackTest(APITestCase): self.assertEqual(response.data['name'], self.rack1.name) + 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}) @@ -1441,13 +1462,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): @@ -1469,6 +1490,7 @@ class InterfaceTemplateTest(APITestCase): data = { 'device_type': self.devicetype.pk, 'name': 'Test Interface Template 4', + 'type': '1000base-t', } url = reverse('dcim-api:interfacetemplate-list') @@ -1486,14 +1508,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', }, ] @@ -1511,6 +1536,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}) @@ -2621,9 +2647,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) @@ -2684,6 +2710,7 @@ class InterfaceTest(APITestCase): data = { 'device': self.device.pk, 'name': 'Test Interface 4', + 'type': '1000base-t', } url = reverse('dcim-api:interface-list') @@ -2700,6 +2727,7 @@ class InterfaceTest(APITestCase): data = { 'device': self.device.pk, 'name': 'Test Interface 4', + 'type': '1000base-t', 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan3.id, 'tagged_vlans': [self.vlan1.id, self.vlan2.id], @@ -2721,14 +2749,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', }, ] @@ -2747,6 +2778,7 @@ class InterfaceTest(APITestCase): { 'device': self.device.pk, 'name': 'Test Interface 4', + 'type': '1000base-t', 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan2.id, 'tagged_vlans': [self.vlan1.id], @@ -2754,6 +2786,7 @@ class InterfaceTest(APITestCase): { 'device': self.device.pk, 'name': 'Test Interface 5', + 'type': '1000base-t', 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan2.id, 'tagged_vlans': [self.vlan1.id], @@ -2761,6 +2794,7 @@ class InterfaceTest(APITestCase): { 'device': self.device.pk, 'name': 'Test Interface 6', + 'type': '1000base-t', 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan2.id, 'tagged_vlans': [self.vlan1.id], @@ -2786,6 +2820,7 @@ class InterfaceTest(APITestCase): data = { 'device': self.device.pk, 'name': 'Test Interface X', + 'type': '1000base-x-gbic', 'lag': lag_interface.pk, } diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index 16c1ca58d..02de313fc 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -11,7 +11,7 @@ from dcim.models import ( VirtualChassis, ) from ipam.models import IPAddress -from tenancy.models import Tenant +from tenancy.models import Tenant, TenantGroup from virtualization.models import Cluster, ClusterType @@ -76,10 +76,24 @@ class SiteTestCase(TestCase): for region in regions: region.save() + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + TenantGroup.objects.bulk_create(tenant_groups) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + 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(name='Site 1', slug='site-1', region=regions[0], tenant=tenants[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], tenant=tenants[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], tenant=tenants[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) @@ -124,11 +138,6 @@ class SiteTestCase(TestCase): 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) @@ -140,6 +149,20 @@ class SiteTestCase(TestCase): params = {'region': [regions[0].slug, regions[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class RackGroupTestCase(TestCase): queryset = RackGroup.objects.all() @@ -266,10 +289,24 @@ class RackTestCase(TestCase): ) RackRole.objects.bulk_create(rack_roles) + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + TenantGroup.objects.bulk_create(tenant_groups) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + 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(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], tenant=tenants[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], tenant=tenants[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], tenant=tenants[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) @@ -323,11 +360,6 @@ class RackTestCase(TestCase): 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]} @@ -366,6 +398,20 @@ class RackTestCase(TestCase): params = {'serial': 'abc'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class RackReservationTestCase(TestCase): queryset = RackReservation.objects.all() @@ -402,18 +448,27 @@ class RackReservationTestCase(TestCase): ) User.objects.bulk_create(users) + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + TenantGroup.objects.bulk_create(tenant_groups) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + 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(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0]), + RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1]), + RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2], tenant=tenants[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]} @@ -436,6 +491,20 @@ class RackReservationTestCase(TestCase): # params = {'user': [users[0].username, users[1].username]} # self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class ManufacturerTestCase(TestCase): queryset = Manufacturer.objects.all() @@ -547,11 +616,6 @@ class DeviceTypeTestCase(TestCase): 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]} @@ -1099,10 +1163,24 @@ class DeviceTestCase(TestCase): ) Cluster.objects.bulk_create(clusters) + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + TenantGroup.objects.bulk_create(tenant_groups) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + 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(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[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], tenant=tenants[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], tenant=tenants[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) @@ -1185,11 +1263,6 @@ class DeviceTestCase(TestCase): 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]} @@ -1333,6 +1406,20 @@ class DeviceTestCase(TestCase): params = {'local_context_data': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class ConsolePortTestCase(TestCase): queryset = ConsolePort.objects.all() diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 32d864a51..c9d5c4c20 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -2,7 +2,6 @@ from django.core.exceptions import ValidationError from django.test import TestCase from dcim.choices import * -from dcim.constants import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_PLANNED from dcim.models import * from tenancy.models import Tenant @@ -522,14 +521,14 @@ 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_PLANNED) + self.assertFalse(interface1.connection_status) # Switch third segment from planned to 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): @@ -542,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_natural_ordering.py b/netbox/dcim/tests/test_natural_ordering.py index d4dca43d7..2d2b5c4dc 100644 --- a/netbox/dcim/tests/test_natural_ordering.py +++ b/netbox/dcim/tests/test_natural_ordering.py @@ -19,14 +19,9 @@ class NaturalOrderingTestCase(TestCase): device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site ) - def _compare_names(self, queryset, names): - - for i, obj in enumerate(queryset): - self.assertEqual(obj.name, names[i]) - def test_interface_ordering_numeric(self): - INTERFACES = ( + INTERFACES = [ '0', '0.1', '0.2', @@ -53,17 +48,20 @@ class NaturalOrderingTestCase(TestCase): '1:2.1', '1:2.2', '1:2.10', - ) + ] for name in INTERFACES: iface = Interface(device=self.device, name=name) iface.save() - self._compare_names(Interface.objects.filter(device=self.device), INTERFACES) + self.assertListEqual( + list(Interface.objects.filter(device=self.device).values_list('name', flat=True)), + INTERFACES + ) def test_interface_ordering_linux(self): - INTERFACES = ( + INTERFACES = [ 'eth0', 'eth0.1', 'eth0.2', @@ -74,17 +72,20 @@ class NaturalOrderingTestCase(TestCase): 'eth1.2', 'eth1.100', 'lo0', - ) + ] for name in INTERFACES: iface = Interface(device=self.device, name=name) iface.save() - self._compare_names(Interface.objects.filter(device=self.device), INTERFACES) + self.assertListEqual( + list(Interface.objects.filter(device=self.device).values_list('name', flat=True)), + INTERFACES + ) def test_interface_ordering_junos(self): - INTERFACES = ( + INTERFACES = [ 'xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', @@ -124,17 +125,20 @@ class NaturalOrderingTestCase(TestCase): 'irb.10', 'irb.100', 'lo0', - ) + ] for name in INTERFACES: iface = Interface(device=self.device, name=name) iface.save() - self._compare_names(Interface.objects.filter(device=self.device), INTERFACES) + self.assertListEqual( + list(Interface.objects.filter(device=self.device).values_list('name', flat=True)), + INTERFACES + ) def test_interface_ordering_ios(self): - INTERFACES = ( + INTERFACES = [ 'GigabitEthernet0/1', 'GigabitEthernet0/2', 'GigabitEthernet0/10', @@ -148,10 +152,13 @@ class NaturalOrderingTestCase(TestCase): 'FastEthernet1', 'FastEthernet2', 'FastEthernet10', - ) + ] for name in INTERFACES: iface = Interface(device=self.device, name=name) iface.save() - self._compare_names(Interface.objects.filter(device=self.device), INTERFACES) + self.assertListEqual( + list(Interface.objects.filter(device=self.device).values_list('name', flat=True)), + INTERFACES + ) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 704dedb40..2263a472b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -176,9 +176,6 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): test_get_object = None test_create_object = None - # TODO: Fix URL name for view - test_import_objects = None - @classmethod def setUpTestData(cls): @@ -204,6 +201,13 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'description': 'Rack reservation', } + cls.csv_data = ( + 'site,rack_name,units,description', + 'Site 1,Rack 1,"10,11,12",Reservation 1', + 'Site 1,Rack 1,"13,14,15",Reservation 2', + 'Site 1,Rack 1,"16,17,18",Reservation 3', + ) + cls.bulk_edit_data = { 'user': user3.pk, 'tenant': None, @@ -1553,9 +1557,6 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase): class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = PowerPanel - # Disable inapplicable tests - test_bulk_edit_objects = None - @classmethod def setUpTestData(cls): @@ -1590,6 +1591,11 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase): "Site 1,Rack Group 1,Power Panel 6", ) + cls.bulk_edit_data = { + 'site': sites[1].pk, + 'rack_group': rackgroups[1].pk, + } + class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = PowerFeed diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 165ca9e02..456905691 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -51,6 +51,7 @@ urlpatterns = [ # Rack reservations path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), + path('rack-reservations/import/', views.RackReservationImportView.as_view(), name='rackreservation_import'), path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), path('rack-reservations//edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), @@ -331,6 +332,7 @@ urlpatterns = [ 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/edit/', views.PowerPanelBulkEditView.as_view(), name='powerpanel_bulk_edit'), 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'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 91b32bc70..a639849ba 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -357,7 +357,7 @@ class RackElevationListView(PermissionRequiredMixin, View): def get(self, request): - racks = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role', 'devices__device_type') + racks = Rack.objects.prefetch_related('role') racks = filters.RackFilterSet(request.GET, racks).qs total_count = racks.count() @@ -470,7 +470,7 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView): filterset = filters.RackReservationFilterSet filterset_form = forms.RackReservationFilterForm table = tables.RackReservationTable - action_buttons = () + action_buttons = ('export',) class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView): @@ -500,6 +500,23 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): return obj.rack.get_absolute_url() +class RackReservationImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_rackreservation' + model_form = forms.RackReservationCSVForm + table = tables.RackReservationTable + default_return_url = 'dcim:rackreservation_list' + + def _save_obj(self, obj_form, request): + """ + Assign the currently authenticated user to the RackReservation. + """ + instance = obj_form.save(commit=False) + instance.user = request.user + instance.save() + + return instance + + class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rackreservation' queryset = RackReservation.objects.prefetch_related('rack', 'user') @@ -1245,7 +1262,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView): template_name = 'dcim/device_import_child.html' default_return_url = 'dcim:device_list' - def _save_obj(self, obj_form): + def _save_obj(self, obj_form, request): obj = obj_form.save() @@ -1316,6 +1333,7 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView): class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_consoleport' queryset = ConsolePort.objects.all() + filterset = filters.ConsolePortFilterSet table = tables.ConsolePortTable form = forms.ConsolePortBulkEditForm @@ -1323,6 +1341,7 @@ class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView): class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleport' queryset = ConsolePort.objects.all() + filterset = filters.ConsolePortFilterSet table = tables.ConsolePortTable default_return_url = 'dcim:consoleport_list' @@ -1369,6 +1388,7 @@ class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView): class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_consoleserverport' queryset = ConsoleServerPort.objects.all() + filterset = filters.ConsoleServerPortFilterSet table = tables.ConsoleServerPortTable form = forms.ConsoleServerPortBulkEditForm @@ -1388,6 +1408,7 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleserverport' queryset = ConsoleServerPort.objects.all() + filterset = filters.ConsoleServerPortFilterSet table = tables.ConsoleServerPortTable default_return_url = 'dcim:consoleserverport_list' @@ -1434,6 +1455,7 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView): class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_powerport' queryset = PowerPort.objects.all() + filterset = filters.PowerPortFilterSet table = tables.PowerPortTable form = forms.PowerPortBulkEditForm @@ -1441,6 +1463,7 @@ class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView): class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerport' queryset = PowerPort.objects.all() + filterset = filters.PowerPortFilterSet table = tables.PowerPortTable default_return_url = 'dcim:powerport_list' @@ -1487,6 +1510,7 @@ class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView): class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_poweroutlet' queryset = PowerOutlet.objects.all() + filterset = filters.PowerOutletFilterSet table = tables.PowerOutletTable form = forms.PowerOutletBulkEditForm @@ -1506,6 +1530,7 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView) class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_poweroutlet' queryset = PowerOutlet.objects.all() + filterset = filters.PowerOutletFilterSet table = tables.PowerOutletTable default_return_url = 'dcim:poweroutlet_list' @@ -1589,6 +1614,7 @@ class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_interface' queryset = Interface.objects.all() + filterset = filters.InterfaceFilterSet table = tables.InterfaceTable form = forms.InterfaceBulkEditForm @@ -1608,6 +1634,7 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_interface' queryset = Interface.objects.all() + filterset = filters.InterfaceFilterSet table = tables.InterfaceTable default_return_url = 'dcim:interface_list' @@ -1654,6 +1681,7 @@ class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView): class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_frontport' queryset = FrontPort.objects.all() + filterset = filters.FrontPortFilterSet table = tables.FrontPortTable form = forms.FrontPortBulkEditForm @@ -1673,6 +1701,7 @@ class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_frontport' queryset = FrontPort.objects.all() + filterset = filters.FrontPortFilterSet table = tables.FrontPortTable default_return_url = 'dcim:frontport_list' @@ -1719,6 +1748,7 @@ class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView): class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rearport' queryset = RearPort.objects.all() + filterset = filters.RearPortFilterSet table = tables.RearPortTable form = forms.RearPortBulkEditForm @@ -1738,6 +1768,7 @@ class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rearport' queryset = RearPort.objects.all() + filterset = filters.RearPortFilterSet table = tables.RearPortTable default_return_url = 'dcim:rearport_list' @@ -1861,6 +1892,7 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicebay' queryset = DeviceBay.objects.all() + filterset = filters.DeviceBayFilterSet table = tables.DeviceBayTable default_return_url = 'dcim:devicebay_list' @@ -2569,6 +2601,15 @@ class PowerPanelBulkImportView(PermissionRequiredMixin, BulkImportView): default_return_url = 'dcim:powerpanel_list' +class PowerPanelBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_powerpanel' + queryset = PowerPanel.objects.prefetch_related('site', 'rack_group') + filterset = filters.PowerPanelFilterSet + table = tables.PowerPanelTable + form = forms.PowerPanelBulkEditForm + default_return_url = 'dcim:powerpanel_list' + + class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerpanel' queryset = PowerPanel.objects.prefetch_related( diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 2a39c207e..f66cc248f 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -26,7 +26,7 @@ class WebhookForm(forms.ModelForm): class Meta: model = Webhook - exclude = [] + exclude = () def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -38,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()]) diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 9a3041238..5bb1f033d 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,9 +1,11 @@ from datetime import datetime from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from rest_framework import serializers from rest_framework.exceptions import ValidationError +from rest_framework.fields import CreateOnlyDefault from extras.choices import * from extras.models import CustomField, CustomFieldChoice, CustomFieldValue @@ -14,6 +16,43 @@ from utilities.api import ValidatedModelSerializer # Custom fields # +class CustomFieldDefaultValues: + """ + Return a dictionary of all CustomFields assigned to the parent model and their default values. + """ + def __call__(self): + + # Retrieve the CustomFields for the parent model + content_type = ContentType.objects.get_for_model(self.model) + fields = CustomField.objects.filter(obj_type=content_type) + + # Populate the default value for each CustomField + value = {} + for field in fields: + if field.default: + if field.type == CustomFieldTypeChoices.TYPE_INTEGER: + field_value = int(field.default) + elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN: + # TODO: Fix default value assignment for boolean custom fields + field_value = False if field.default.lower() == 'false' else bool(field.default) + elif field.type == CustomFieldTypeChoices.TYPE_SELECT: + try: + field_value = field.choices.get(value=field.default).pk + except ObjectDoesNotExist: + # Invalid default value + field_value = None + else: + field_value = field.default + value[field.name] = field_value + else: + value[field.name] = None + + return value + + def set_context(self, serializer_field): + self.model = serializer_field.parent.Meta.model + + class CustomFieldsSerializer(serializers.BaseSerializer): def to_representation(self, obj): @@ -94,53 +133,35 @@ class CustomFieldModelSerializer(ValidatedModelSerializer): """ Extends ModelSerializer to render any CustomFields and their values associated with an object. """ - custom_fields = CustomFieldsSerializer(required=False) + custom_fields = CustomFieldsSerializer( + required=False, + default=CreateOnlyDefault(CustomFieldDefaultValues()) + ) def __init__(self, *args, **kwargs): - - def _populate_custom_fields(instance, fields): - instance.custom_fields = {} - for field in fields: - value = instance.cf.get(field.name) - 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 - super().__init__(*args, **kwargs) - # Retrieve the set of CustomFields which apply to this type of object - content_type = ContentType.objects.get_for_model(self.Meta.model) - fields = CustomField.objects.filter(obj_type=content_type) - if self.instance is not None: + # Retrieve the set of CustomFields which apply to this type of object + content_type = ContentType.objects.get_for_model(self.Meta.model) + fields = CustomField.objects.filter(obj_type=content_type) + # Populate CustomFieldValues for each instance from database try: for obj in self.instance: - _populate_custom_fields(obj, fields) + self._populate_custom_fields(obj, fields) except TypeError: - _populate_custom_fields(self.instance, fields) + self._populate_custom_fields(self.instance, fields) - else: - - if not hasattr(self, 'initial_data'): - self.initial_data = {} - - # Populate default values - if fields and 'custom_fields' not in self.initial_data: - self.initial_data['custom_fields'] = {} - - # 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 == CustomFieldTypeChoices.TYPE_SELECT: - field_value = field.choices.get(value=field.default).pk - elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN: - field_value = bool(field.default) - else: - field_value = field.default - self.initial_data['custom_fields'][field.name] = field_value + def _populate_custom_fields(self, instance, custom_fields): + instance.custom_fields = {} + for field in custom_fields: + value = instance.cf.get(field.name) + 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 def _save_custom_fields(self, instance, custom_fields): content_type = ContentType.objects.get_for_model(self.Meta.model) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 167768861..aa9e380ba 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -14,7 +14,7 @@ from extras.models import ( 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 extras.scripts import get_script, get_scripts, run_script from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet from . import serializers @@ -265,8 +265,9 @@ class ScriptViewSet(ViewSet): input_serializer = serializers.ScriptInputSerializer(data=request.data) if input_serializer.is_valid(): - output = script.run(input_serializer.data['data']) - script.output = output + data = input_serializer.data['data'] + commit = input_serializer.data['commit'] + script.output, execution_time = run_script(script, data, request, commit) output_serializer = serializers.ScriptOutputSerializer(script) return Response(output_serializer.data) diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index 0ae53f03d..9811cc0b0 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -124,17 +124,18 @@ class TemplateLanguageChoices(ChoiceSet): # Webhooks # -class WebhookContentTypeChoices(ChoiceSet): +class WebhookHttpMethodChoices(ChoiceSet): - CONTENTTYPE_JSON = 'application/json' - CONTENTTYPE_FORMDATA = 'application/x-www-form-urlencoded' + METHOD_GET = 'GET' + METHOD_POST = 'POST' + METHOD_PUT = 'PUT' + METHOD_PATCH = 'PATCH' + METHOD_DELETE = 'DELETE' CHOICES = ( - (CONTENTTYPE_JSON, 'JSON'), - (CONTENTTYPE_FORMDATA, 'Form data'), + (METHOD_GET, 'GET'), + (METHOD_POST, 'POST'), + (METHOD_PUT, 'PUT'), + (METHOD_PATCH, 'PATCH'), + (METHOD_DELETE, 'DELETE'), ) - - LEGACY_MAP = { - CONTENTTYPE_JSON: 1, - CONTENTTYPE_FORMDATA: 2, - } diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index b12bc2f2c..7bb026d34 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -138,6 +138,8 @@ LOG_LEVEL_CODES = { LOG_FAILURE: 'failure', } +HTTP_CONTENT_TYPE_JSON = 'application/json' + # Models which support registered webhooks WEBHOOK_MODELS = Q( Q(app_label='circuits', model__in=[ diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index dcd4f3ede..ad414a691 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -4,6 +4,7 @@ from django.db.models import Q from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup +from utilities.filters import BaseFilterSet from virtualization.models import Cluster, ClusterGroup from .choices import * from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag @@ -89,21 +90,21 @@ class CustomFieldFilterSet(django_filters.FilterSet): self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf) -class GraphFilterSet(django_filters.FilterSet): +class GraphFilterSet(BaseFilterSet): class Meta: model = Graph fields = ['type', 'name', 'template_language'] -class ExportTemplateFilterSet(django_filters.FilterSet): +class ExportTemplateFilterSet(BaseFilterSet): class Meta: model = ExportTemplate fields = ['content_type', 'name', 'template_language'] -class TagFilterSet(django_filters.FilterSet): +class TagFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -122,7 +123,7 @@ class TagFilterSet(django_filters.FilterSet): ) -class ConfigContextFilterSet(django_filters.FilterSet): +class ConfigContextFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -244,7 +245,7 @@ class LocalConfigContextFilterSet(django_filters.FilterSet): return queryset.exclude(local_context_data__isnull=value) -class ObjectChangeFilterSet(django_filters.FilterSet): +class ObjectChangeFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/extras/middleware.py b/netbox/extras/middleware.py index 3624e11a5..c8be8755f 100644 --- a/netbox/extras/middleware.py +++ b/netbox/extras/middleware.py @@ -5,11 +5,14 @@ from copy import deepcopy from datetime import timedelta from django.conf import settings +from django.contrib import messages from django.db.models.signals import pre_delete, post_save from django.utils import timezone from django_prometheus.models import model_deletes, model_inserts, model_updates +from redis.exceptions import RedisError from extras.utils import is_taggable +from utilities.api import is_api_request from utilities.querysets import DummyQuerySet from .choices import ObjectChangeActionChoices from .models import ObjectChange @@ -98,7 +101,12 @@ class ObjectChangeMiddleware(object): if not _thread_locals.changed_objects: return response + # Disconnect our receivers from the post_save and post_delete signals. + post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object') + pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object') + # Create records for any cached objects that were changed. + redis_failed = False for instance, action in _thread_locals.changed_objects: # Refresh cached custom field values @@ -114,7 +122,16 @@ class ObjectChangeMiddleware(object): objectchange.save() # Enqueue webhooks - enqueue_webhooks(instance, request.user, request.id, action) + try: + enqueue_webhooks(instance, request.user, request.id, action) + except RedisError as e: + if not redis_failed and not is_api_request(request): + messages.error( + request, + "There was an error processing webhooks for this request. Check that the Redis service is " + "running and reachable. The full error details were: {}".format(e) + ) + redis_failed = True # Increment metric counters if action == ObjectChangeActionChoices.ACTION_CREATE: diff --git a/netbox/extras/migrations/0038_webhook_template_support.py b/netbox/extras/migrations/0038_webhook_template_support.py new file mode 100644 index 000000000..7d563820f --- /dev/null +++ b/netbox/extras/migrations/0038_webhook_template_support.py @@ -0,0 +1,48 @@ +import json + +from django.db import migrations, models + + +def json_to_text(apps, schema_editor): + """ + Convert a JSON representation of HTTP headers to key-value pairs (one header per line) + """ + Webhook = apps.get_model('extras', 'Webhook') + for webhook in Webhook.objects.exclude(additional_headers=''): + data = json.loads(webhook.additional_headers) + headers = ['{}: {}'.format(k, v) for k, v in data.items()] + Webhook.objects.filter(pk=webhook.pk).update(additional_headers='\n'.join(headers)) + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0037_configcontexts_clusters'), + ] + + operations = [ + migrations.AddField( + model_name='webhook', + name='http_method', + field=models.CharField(default='POST', max_length=30), + ), + migrations.AddField( + model_name='webhook', + name='body_template', + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name='webhook', + name='additional_headers', + field=models.TextField(blank=True, default=''), + preserve_default=False, + ), + migrations.AlterField( + model_name='webhook', + name='http_content_type', + field=models.CharField(default='application/json', max_length=100), + ), + migrations.RunPython( + code=json_to_text + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 5d175d172..d81fbeab9 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -1,3 +1,4 @@ +import json from collections import OrderedDict from datetime import date @@ -12,6 +13,7 @@ from django.http import HttpResponse from django.template import Template, Context from django.urls import reverse from django.utils.text import slugify +from rest_framework.utils.encoders import JSONEncoder from taggit.models import TagBase, GenericTaggedItemBase from utilities.fields import ColorField @@ -52,7 +54,6 @@ class Webhook(models.Model): delete in NetBox. The request will contain a representation of the object, which the remote application can act on. Each Webhook can be limited to firing only on certain actions or certain object types. """ - obj_type = models.ManyToManyField( to=ContentType, related_name='webhooks', @@ -81,17 +82,33 @@ class Webhook(models.Model): verbose_name='URL', help_text="A POST will be sent to this URL when the webhook is called." ) - http_content_type = models.CharField( - max_length=50, - choices=WebhookContentTypeChoices, - default=WebhookContentTypeChoices.CONTENTTYPE_JSON, - verbose_name='HTTP content type' + enabled = models.BooleanField( + default=True ) - additional_headers = JSONField( - null=True, + http_method = models.CharField( + max_length=30, + choices=WebhookHttpMethodChoices, + default=WebhookHttpMethodChoices.METHOD_POST, + verbose_name='HTTP method' + ) + http_content_type = models.CharField( + max_length=100, + default=HTTP_CONTENT_TYPE_JSON, + verbose_name='HTTP content type', + help_text='The complete list of official content types is available ' + 'here.' + ) + additional_headers = models.TextField( blank=True, - help_text="User supplied headers which should be added to the request in addition to the HTTP content type. " - "Headers are supplied as key/value pairs in a JSON object." + help_text="User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. " + "Headers should be defined in the format Name: Value. Jinja2 template processing is " + "support with the same context as the request body (below)." + ) + body_template = models.TextField( + blank=True, + help_text='Jinja2 template for a custom request body. If blank, a JSON object representing the change will be ' + 'included. Available context data includes: event, model, ' + 'timestamp, username, request_id, and data.' ) secret = models.CharField( max_length=255, @@ -101,9 +118,6 @@ class Webhook(models.Model): "the secret as the key. The secret is not transmitted in " "the request." ) - enabled = models.BooleanField( - default=True - ) ssl_verification = models.BooleanField( default=True, verbose_name='SSL verification', @@ -126,9 +140,6 @@ class Webhook(models.Model): return self.name def clean(self): - """ - Validate model - """ if not self.type_create and not self.type_delete and not self.type_update: raise ValidationError( "You must select at least one type: create, update, and/or delete." @@ -136,14 +147,30 @@ class Webhook(models.Model): if not self.ssl_verification and self.ca_file_path: raise ValidationError({ - 'ca_file_path': 'Do not specify a CA certificate file if SSL verification is dissabled.' + 'ca_file_path': 'Do not specify a CA certificate file if SSL verification is disabled.' }) - # Verify that JSON data is provided as an object - if self.additional_headers and type(self.additional_headers) is not dict: - raise ValidationError({ - 'additional_headers': 'Header JSON data must be in object form. Example: {"X-API-KEY": "abc123"}' - }) + def render_headers(self, context): + """ + Render additional_headers and return a dict of Header: Value pairs. + """ + if not self.additional_headers: + return {} + ret = {} + data = render_jinja2(self.additional_headers, context) + for line in data.splitlines(): + header, value = line.split(':') + ret[header.strip()] = value.strip() + return ret + + def render_body(self, context): + """ + Render the body template, if defined. Otherwise, jump the context as a JSON object. + """ + if self.body_template: + return render_jinja2(self.body_template, context) + else: + return json.dumps(context, cls=JSONEncoder) # diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py index 9027d6a4a..373acdde7 100644 --- a/netbox/extras/reports.py +++ b/netbox/extras/reports.py @@ -1,5 +1,6 @@ import importlib import inspect +import logging import pkgutil from collections import OrderedDict @@ -91,6 +92,8 @@ class Report(object): self.active_test = None self.failed = False + self.logger = logging.getLogger(f"netbox.reports.{self.module}.{self.name}") + # Compile test methods and initialize results skeleton test_methods = [] for method in dir(self): @@ -138,6 +141,7 @@ class Report(object): Log a message which is not associated with a particular object. """ self._log(None, message, level=LOG_DEFAULT) + self.logger.info(message) def log_success(self, obj, message=None): """ @@ -146,6 +150,7 @@ class Report(object): if message: self._log(obj, message, level=LOG_SUCCESS) self._results[self.active_test]['success'] += 1 + self.logger.info(f"Success | {obj}: {message}") def log_info(self, obj, message): """ @@ -153,6 +158,7 @@ class Report(object): """ self._log(obj, message, level=LOG_INFO) self._results[self.active_test]['info'] += 1 + self.logger.info(f"Info | {obj}: {message}") def log_warning(self, obj, message): """ @@ -160,6 +166,7 @@ class Report(object): """ self._log(obj, message, level=LOG_WARNING) self._results[self.active_test]['warning'] += 1 + self.logger.info(f"Warning | {obj}: {message}") def log_failure(self, obj, message): """ @@ -167,12 +174,15 @@ class Report(object): """ self._log(obj, message, level=LOG_FAILURE) self._results[self.active_test]['failure'] += 1 + self.logger.info(f"Failure | {obj}: {message}") self.failed = True def run(self): """ Run the report and return its results. Each test method will be executed in order. """ + self.logger.info(f"Running report") + for method_name in self.test_methods: self.active_test = method_name test_method = getattr(self, method_name) @@ -184,6 +194,11 @@ class Report(object): result.save() self.result = result + if self.failed: + self.logger.warning("Report failed") + else: + self.logger.info("Report completed successfully") + # Perform any post-run tasks self.post_run() diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index e5a32bde6..99236bf9c 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -1,5 +1,6 @@ import inspect import json +import logging import os import pkgutil import time @@ -63,10 +64,6 @@ class ScriptVariable: self.field_attrs['widget'] = widget self.field_attrs['required'] = required - # Initialize the list of optional validators if none have already been defined - if 'validators' not in self.field_attrs: - self.field_attrs['validators'] = [] - def as_field(self): """ Render the variable as a Django form field. @@ -227,14 +224,12 @@ class IPNetworkVar(ScriptVariable): An IPv4 or IPv6 prefix. """ form_field = IPNetworkFormField - field_attrs = { - 'validators': [prefix_validator] - } def __init__(self, min_prefix_length=None, max_prefix_length=None, *args, **kwargs): super().__init__(*args, **kwargs) - # Optional minimum/maximum prefix lengths + # Set prefix validator and optional minimum/maximum prefix lengths + self.field_attrs['validators'] = [prefix_validator] if min_prefix_length is not None: self.field_attrs['validators'].append( MinPrefixLengthValidator(min_prefix_length) @@ -260,6 +255,7 @@ class BaseScript: def __init__(self): # Initiate the log + self.logger = logging.getLogger(f"netbox.scripts.{self.module()}.{self.__class__.__name__}") self.log = [] # Declare the placeholder for the current request @@ -292,7 +288,7 @@ class BaseScript: return vars - def run(self, data): + def run(self, data, commit): raise NotImplementedError("The script must define a run() method.") def as_form(self, data=None, files=None, initial=None): @@ -307,18 +303,23 @@ class BaseScript: # Logging def log_debug(self, message): + self.logger.log(logging.DEBUG, message) self.log.append((LOG_DEFAULT, message)) def log_success(self, message): + self.logger.log(logging.INFO, message) # No syslog equivalent for SUCCESS self.log.append((LOG_SUCCESS, message)) def log_info(self, message): + self.logger.log(logging.INFO, message) self.log.append((LOG_INFO, message)) def log_warning(self, message): + self.logger.log(logging.WARNING, message) self.log.append((LOG_WARNING, message)) def log_failure(self, message): + self.logger.log(logging.ERROR, message) self.log.append((LOG_FAILURE, message)) # Convenience functions @@ -381,6 +382,10 @@ def run_script(script, data, request, commit=True): start_time = None end_time = None + script_name = script.__class__.__name__ + logger = logging.getLogger(f"netbox.scripts.{script.module()}.{script_name}") + logger.info(f"Running script (commit={commit})") + # Add files to form data files = request.FILES for field_name, fileobj in files.items(): @@ -389,10 +394,17 @@ def run_script(script, data, request, commit=True): # Add the current request as a property of the script script.request = request + # Determine whether the script accepts a 'commit' argument (this was introduced in v2.7.8) + kwargs = { + 'data': data + } + if 'commit' in inspect.signature(script.run).parameters: + kwargs['commit'] = commit + try: with transaction.atomic(): start_time = time.time() - output = script.run(data) + output = script.run(**kwargs) end_time = time.time() if not commit: raise AbortTransaction() @@ -403,6 +415,7 @@ def run_script(script, data, request, commit=True): script.log_failure( "An exception occurred: `{}: {}`\n```\n{}\n```".format(type(e).__name__, e, stacktrace) ) + logger.error(f"Exception raised during script execution: {e}") commit = False finally: if not commit: @@ -415,6 +428,7 @@ def run_script(script, data, request, commit=True): # Calculate execution time if end_time is not None: execution_time = end_time - start_time + logger.info(f"Script completed in {execution_time:.4f} seconds") else: execution_time = None @@ -437,7 +451,8 @@ def get_scripts(use_names=False): module_scripts = OrderedDict() for name, cls in inspect.getmembers(module, is_script): module_scripts[name] = cls - scripts[module_name] = module_scripts + if module_scripts: + scripts[module_name] = module_scripts return scripts diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 9d25ce576..abc2c684f 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -582,7 +582,7 @@ class ScriptTest(APITestCase): var2 = IntegerVar() var3 = BooleanVar() - def run(self, data): + def run(self, data, commit=True): self.log_info(data['var1']) self.log_success(data['var2']) diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index a6e2bfcec..d76532437 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -101,240 +101,329 @@ class CustomFieldTest(TestCase): class CustomFieldAPITest(APITestCase): - def setUp(self): - - super().setUp() - + @classmethod + def setUpTestData(cls): content_type = ContentType.objects.get_for_model(Site) # Text custom field - self.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='magic_word') - self.cf_text.save() - self.cf_text.obj_type.set([content_type]) - self.cf_text.save() + cls.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo') + cls.cf_text.save() + cls.cf_text.obj_type.set([content_type]) # Integer custom field - self.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='magic_number') - self.cf_integer.save() - self.cf_integer.obj_type.set([content_type]) - self.cf_integer.save() + cls.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123) + cls.cf_integer.save() + cls.cf_integer.obj_type.set([content_type]) # Boolean custom field - self.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='is_magic') - self.cf_boolean.save() - self.cf_boolean.obj_type.set([content_type]) - self.cf_boolean.save() + cls.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False) + cls.cf_boolean.save() + cls.cf_boolean.obj_type.set([content_type]) # Date custom field - self.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='magic_date') - self.cf_date.save() - self.cf_date.obj_type.set([content_type]) - self.cf_date.save() + cls.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01') + cls.cf_date.save() + cls.cf_date.obj_type.set([content_type]) # URL custom field - self.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='magic_url') - self.cf_url.save() - self.cf_url.obj_type.set([content_type]) - self.cf_url.save() + cls.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1') + cls.cf_url.save() + cls.cf_url.obj_type.set([content_type]) # Select custom field - self.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='magic_choice') - self.cf_select.save() - self.cf_select.obj_type.set([content_type]) - self.cf_select.save() - self.cf_select_choice1 = CustomFieldChoice(field=self.cf_select, value='Foo') - self.cf_select_choice1.save() - self.cf_select_choice2 = CustomFieldChoice(field=self.cf_select, value='Bar') - self.cf_select_choice2.save() - self.cf_select_choice3 = CustomFieldChoice(field=self.cf_select, value='Baz') - self.cf_select_choice3.save() + cls.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='choice_field') + cls.cf_select.save() + cls.cf_select.obj_type.set([content_type]) + cls.cf_select_choice1 = CustomFieldChoice(field=cls.cf_select, value='Foo') + cls.cf_select_choice1.save() + cls.cf_select_choice2 = CustomFieldChoice(field=cls.cf_select, value='Bar') + cls.cf_select_choice2.save() + cls.cf_select_choice3 = CustomFieldChoice(field=cls.cf_select, value='Baz') + cls.cf_select_choice3.save() - self.site = Site.objects.create(name='Test Site 1', slug='test-site-1') + cls.cf_select.default = cls.cf_select_choice1.value + cls.cf_select.save() - def test_get_obj_without_custom_fields(self): + # Create some sites + cls.sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(cls.sites) - url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) - response = self.client.get(url, **self.header) - - self.assertEqual(response.data['name'], self.site.name) - self.assertEqual(response.data['custom_fields'], { - 'magic_word': None, - 'magic_number': None, - 'is_magic': None, - 'magic_date': None, - 'magic_url': None, - 'magic_choice': None, - }) - - def test_get_obj_with_custom_fields(self): - - CUSTOM_FIELD_VALUES = [ - (self.cf_text, 'Test string'), - (self.cf_integer, 1234), - (self.cf_boolean, True), - (self.cf_date, date(2016, 6, 23)), - (self.cf_url, 'http://example.com/'), - (self.cf_select, self.cf_select_choice1.pk), - ] - for field, value in CUSTOM_FIELD_VALUES: - cfv = CustomFieldValue(field=field, obj=self.site) + # Assign custom field values for site 2 + site2_cfvs = { + cls.cf_text: 'bar', + cls.cf_integer: 456, + cls.cf_boolean: True, + cls.cf_date: '2020-01-02', + cls.cf_url: 'http://example.com/2', + cls.cf_select: cls.cf_select_choice2.pk, + } + for field, value in site2_cfvs.items(): + cfv = CustomFieldValue(field=field, obj=cls.sites[1]) cfv.value = value cfv.save() - url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) + def test_get_single_object_without_custom_field_values(self): + """ + Validate that custom fields are present on an object even if it has no values defined. + """ + url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[0].pk}) response = self.client.get(url, **self.header) - self.assertEqual(response.data['name'], self.site.name) - self.assertEqual(response.data['custom_fields'].get('magic_word'), CUSTOM_FIELD_VALUES[0][1]) - self.assertEqual(response.data['custom_fields'].get('magic_number'), CUSTOM_FIELD_VALUES[1][1]) - self.assertEqual(response.data['custom_fields'].get('is_magic'), CUSTOM_FIELD_VALUES[2][1]) - self.assertEqual(response.data['custom_fields'].get('magic_date'), CUSTOM_FIELD_VALUES[3][1]) - self.assertEqual(response.data['custom_fields'].get('magic_url'), CUSTOM_FIELD_VALUES[4][1]) - self.assertEqual(response.data['custom_fields'].get('magic_choice'), { - 'value': self.cf_select_choice1.pk, 'label': 'Foo' + self.assertEqual(response.data['name'], self.sites[0].name) + self.assertEqual(response.data['custom_fields'], { + 'text_field': None, + 'number_field': None, + 'boolean_field': None, + 'date_field': None, + 'url_field': None, + 'choice_field': None, }) - def test_set_custom_field_text(self): - - data = { - 'name': 'Test Site 1', - 'slug': 'test-site-1', - 'custom_fields': { - 'magic_word': 'Foo bar baz', - } - } - - url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) - response = self.client.put(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(response.data['custom_fields'].get('magic_word'), data['custom_fields']['magic_word']) - cfv = self.site.custom_field_values.get(field=self.cf_text) - self.assertEqual(cfv.value, data['custom_fields']['magic_word']) - - def test_set_custom_field_integer(self): - - data = { - 'name': 'Test Site 1', - 'slug': 'test-site-1', - 'custom_fields': { - 'magic_number': 42, - } - } - - url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) - response = self.client.put(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(response.data['custom_fields'].get('magic_number'), data['custom_fields']['magic_number']) - cfv = self.site.custom_field_values.get(field=self.cf_integer) - self.assertEqual(cfv.value, data['custom_fields']['magic_number']) - - def test_set_custom_field_boolean(self): - - data = { - 'name': 'Test Site 1', - 'slug': 'test-site-1', - 'custom_fields': { - 'is_magic': 0, - } - } - - url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) - response = self.client.put(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(response.data['custom_fields'].get('is_magic'), data['custom_fields']['is_magic']) - cfv = self.site.custom_field_values.get(field=self.cf_boolean) - self.assertEqual(cfv.value, data['custom_fields']['is_magic']) - - def test_set_custom_field_date(self): - - data = { - 'name': 'Test Site 1', - 'slug': 'test-site-1', - 'custom_fields': { - 'magic_date': '2017-04-25', - } - } - - url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) - response = self.client.put(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(response.data['custom_fields'].get('magic_date'), data['custom_fields']['magic_date']) - cfv = self.site.custom_field_values.get(field=self.cf_date) - self.assertEqual(cfv.value.isoformat(), data['custom_fields']['magic_date']) - - def test_set_custom_field_url(self): - - data = { - 'name': 'Test Site 1', - 'slug': 'test-site-1', - 'custom_fields': { - 'magic_url': 'http://example.com/2/', - } - } - - url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) - response = self.client.put(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(response.data['custom_fields'].get('magic_url'), data['custom_fields']['magic_url']) - cfv = self.site.custom_field_values.get(field=self.cf_url) - self.assertEqual(cfv.value, data['custom_fields']['magic_url']) - - def test_set_custom_field_select(self): - - data = { - 'name': 'Test Site 1', - 'slug': 'test-site-1', - 'custom_fields': { - 'magic_choice': self.cf_select_choice2.pk, - } - } - - url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) - response = self.client.put(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(response.data['custom_fields'].get('magic_choice'), data['custom_fields']['magic_choice']) - cfv = self.site.custom_field_values.get(field=self.cf_select) - self.assertEqual(cfv.value.pk, data['custom_fields']['magic_choice']) - - def test_set_custom_field_defaults(self): + def test_get_single_object_with_custom_field_values(self): """ - Create a new object with no custom field data. Custom field values should be created using the custom fields' - default values. + Validate that custom fields are present and correctly set for an object with values defined. """ - CUSTOM_FIELD_DEFAULTS = { - 'magic_word': 'foobar', - 'magic_number': '123', - 'is_magic': 'true', - 'magic_date': '2019-12-13', - 'magic_url': 'http://example.com/', - 'magic_choice': self.cf_select_choice1.value, + site2_cfvs = { + cfv.field.name: cfv.value for cfv in self.sites[1].custom_field_values.all() } - # Update CustomFields to set default values - for field_name, default_value in CUSTOM_FIELD_DEFAULTS.items(): - CustomField.objects.filter(name=field_name).update(default=default_value) + url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk}) + response = self.client.get(url, **self.header) + self.assertEqual(response.data['name'], self.sites[1].name) + self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field']) + self.assertEqual(response.data['custom_fields']['number_field'], site2_cfvs['number_field']) + self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field']) + self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field']) + self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field']) + self.assertEqual(response.data['custom_fields']['choice_field']['label'], self.cf_select_choice2.value) + + def test_create_single_object_with_defaults(self): + """ + Create a new site with no specified custom field values and check that it received the default values. + """ data = { - 'name': 'Test Site X', - 'slug': 'test-site-x', + 'name': 'Site 3', + 'slug': 'site-3', } url = reverse('dcim-api:site-list') response = self.client.post(url, data, format='json', **self.header) - self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(response.data['custom_fields']['magic_word'], CUSTOM_FIELD_DEFAULTS['magic_word']) - self.assertEqual(response.data['custom_fields']['magic_number'], str(CUSTOM_FIELD_DEFAULTS['magic_number'])) - self.assertEqual(response.data['custom_fields']['is_magic'], bool(CUSTOM_FIELD_DEFAULTS['is_magic'])) - self.assertEqual(response.data['custom_fields']['magic_date'], CUSTOM_FIELD_DEFAULTS['magic_date']) - self.assertEqual(response.data['custom_fields']['magic_url'], CUSTOM_FIELD_DEFAULTS['magic_url']) - self.assertEqual(response.data['custom_fields']['magic_choice'], self.cf_select_choice1.pk) + + # Validate response data + response_cf = response.data['custom_fields'] + self.assertEqual(response_cf['text_field'], self.cf_text.default) + self.assertEqual(response_cf['number_field'], self.cf_integer.default) + self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default) + self.assertEqual(response_cf['date_field'], self.cf_date.default) + self.assertEqual(response_cf['url_field'], self.cf_url.default) + self.assertEqual(response_cf['choice_field'], self.cf_select_choice1.pk) + + # Validate database data + site = Site.objects.get(pk=response.data['id']) + cfvs = { + cfv.field.name: cfv.value for cfv in site.custom_field_values.all() + } + self.assertEqual(cfvs['text_field'], self.cf_text.default) + self.assertEqual(cfvs['number_field'], self.cf_integer.default) + self.assertEqual(cfvs['boolean_field'], self.cf_boolean.default) + self.assertEqual(str(cfvs['date_field']), self.cf_date.default) + self.assertEqual(cfvs['url_field'], self.cf_url.default) + self.assertEqual(cfvs['choice_field'].pk, self.cf_select_choice1.pk) + + def test_create_single_object_with_values(self): + """ + Create a single new site with a value for each type of custom field. + """ + data = { + 'name': 'Site 3', + 'slug': 'site-3', + 'custom_fields': { + 'text_field': 'bar', + 'number_field': 456, + 'boolean_field': True, + 'date_field': '2020-01-02', + 'url_field': 'http://example.com/2', + 'choice_field': self.cf_select_choice2.pk, + }, + } + + url = reverse('dcim-api:site-list') + response = self.client.post(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_201_CREATED) + + # Validate response data + response_cf = response.data['custom_fields'] + data_cf = data['custom_fields'] + self.assertEqual(response_cf['text_field'], data_cf['text_field']) + self.assertEqual(response_cf['number_field'], data_cf['number_field']) + self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field']) + self.assertEqual(response_cf['date_field'], data_cf['date_field']) + self.assertEqual(response_cf['url_field'], data_cf['url_field']) + self.assertEqual(response_cf['choice_field'], data_cf['choice_field']) + + # Validate database data + site = Site.objects.get(pk=response.data['id']) + cfvs = { + cfv.field.name: cfv.value for cfv in site.custom_field_values.all() + } + self.assertEqual(cfvs['text_field'], data_cf['text_field']) + self.assertEqual(cfvs['number_field'], data_cf['number_field']) + self.assertEqual(cfvs['boolean_field'], data_cf['boolean_field']) + self.assertEqual(str(cfvs['date_field']), data_cf['date_field']) + self.assertEqual(cfvs['url_field'], data_cf['url_field']) + self.assertEqual(cfvs['choice_field'].pk, data_cf['choice_field']) + + def test_create_multiple_objects_with_defaults(self): + """ + Create three news sites with no specified custom field values and check that each received + the default custom field values. + """ + data = ( + { + 'name': 'Site 3', + 'slug': 'site-3', + }, + { + 'name': 'Site 4', + 'slug': 'site-4', + }, + { + 'name': 'Site 5', + 'slug': 'site-5', + }, + ) + + url = reverse('dcim-api:site-list') + response = self.client.post(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(len(response.data), len(data)) + + for i, obj in enumerate(data): + + # Validate response data + response_cf = response.data[i]['custom_fields'] + self.assertEqual(response_cf['text_field'], self.cf_text.default) + self.assertEqual(response_cf['number_field'], self.cf_integer.default) + self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default) + self.assertEqual(response_cf['date_field'], self.cf_date.default) + self.assertEqual(response_cf['url_field'], self.cf_url.default) + self.assertEqual(response_cf['choice_field'], self.cf_select_choice1.pk) + + # Validate database data + site = Site.objects.get(pk=response.data[i]['id']) + cfvs = { + cfv.field.name: cfv.value for cfv in site.custom_field_values.all() + } + self.assertEqual(cfvs['text_field'], self.cf_text.default) + self.assertEqual(cfvs['number_field'], self.cf_integer.default) + self.assertEqual(cfvs['boolean_field'], self.cf_boolean.default) + self.assertEqual(str(cfvs['date_field']), self.cf_date.default) + self.assertEqual(cfvs['url_field'], self.cf_url.default) + self.assertEqual(cfvs['choice_field'].pk, self.cf_select_choice1.pk) + + def test_create_multiple_objects_with_values(self): + """ + Create a three new sites, each with custom fields defined. + """ + custom_field_data = { + 'text_field': 'bar', + 'number_field': 456, + 'boolean_field': True, + 'date_field': '2020-01-02', + 'url_field': 'http://example.com/2', + 'choice_field': self.cf_select_choice2.pk, + } + data = ( + { + 'name': 'Site 3', + 'slug': 'site-3', + 'custom_fields': custom_field_data, + }, + { + 'name': 'Site 4', + 'slug': 'site-4', + 'custom_fields': custom_field_data, + }, + { + 'name': 'Site 5', + 'slug': 'site-5', + 'custom_fields': custom_field_data, + }, + ) + + url = reverse('dcim-api:site-list') + response = self.client.post(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(len(response.data), len(data)) + + for i, obj in enumerate(data): + + # Validate response data + response_cf = response.data[i]['custom_fields'] + self.assertEqual(response_cf['text_field'], custom_field_data['text_field']) + self.assertEqual(response_cf['number_field'], custom_field_data['number_field']) + self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field']) + self.assertEqual(response_cf['date_field'], custom_field_data['date_field']) + self.assertEqual(response_cf['url_field'], custom_field_data['url_field']) + self.assertEqual(response_cf['choice_field'], custom_field_data['choice_field']) + + # Validate database data + site = Site.objects.get(pk=response.data[i]['id']) + cfvs = { + cfv.field.name: cfv.value for cfv in site.custom_field_values.all() + } + self.assertEqual(cfvs['text_field'], custom_field_data['text_field']) + self.assertEqual(cfvs['number_field'], custom_field_data['number_field']) + self.assertEqual(cfvs['boolean_field'], custom_field_data['boolean_field']) + self.assertEqual(str(cfvs['date_field']), custom_field_data['date_field']) + self.assertEqual(cfvs['url_field'], custom_field_data['url_field']) + self.assertEqual(cfvs['choice_field'].pk, custom_field_data['choice_field']) + + def test_update_single_object_with_values(self): + """ + Update an object with existing custom field values. Ensure that only the updated custom field values are + modified. + """ + site2_original_cfvs = { + cfv.field.name: cfv.value for cfv in self.sites[1].custom_field_values.all() + } + data = { + 'custom_fields': { + 'text_field': 'ABCD', + 'number_field': 1234, + }, + } + + url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk}) + response = self.client.patch(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + + # Validate response data + response_cf = response.data['custom_fields'] + data_cf = data['custom_fields'] + self.assertEqual(response_cf['text_field'], data_cf['text_field']) + self.assertEqual(response_cf['number_field'], data_cf['number_field']) + # TODO: Non-updated fields are missing from the response data + # self.assertEqual(response_cf['boolean_field'], site2_original_cfvs['boolean_field']) + # self.assertEqual(response_cf['date_field'], site2_original_cfvs['date_field']) + # self.assertEqual(response_cf['url_field'], site2_original_cfvs['url_field']) + # self.assertEqual(response_cf['choice_field']['label'], site2_original_cfvs['choice_field'].value) + + # Validate database data + site2_updated_cfvs = { + cfv.field.name: cfv.value for cfv in self.sites[1].custom_field_values.all() + } + self.assertEqual(site2_updated_cfvs['text_field'], data_cf['text_field']) + self.assertEqual(site2_updated_cfvs['number_field'], data_cf['number_field']) + self.assertEqual(site2_updated_cfvs['boolean_field'], site2_original_cfvs['boolean_field']) + self.assertEqual(site2_updated_cfvs['date_field'], site2_original_cfvs['date_field']) + self.assertEqual(site2_updated_cfvs['url_field'], site2_original_cfvs['url_field']) + self.assertEqual(site2_updated_cfvs['choice_field'], site2_original_cfvs['choice_field']) class CustomFieldChoiceAPITest(APITestCase): diff --git a/netbox/extras/tests/test_filters.py b/netbox/extras/tests/test_filters.py index 5ef96faa2..ab559cf73 100644 --- a/netbox/extras/tests/test_filters.py +++ b/netbox/extras/tests/test_filters.py @@ -28,8 +28,8 @@ class GraphTestCase(TestCase): Graph.objects.bulk_create(graphs) def test_name(self): - params = {'name': 'Graph 1'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'name': ['Graph 1', 'Graph 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_type(self): content_type = ContentType.objects.filter(GRAPH_MODELS).first() @@ -59,8 +59,8 @@ class ExportTemplateTestCase(TestCase): ExportTemplate.objects.bulk_create(export_templates) def test_name(self): - params = {'name': 'Export Template 1'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'name': ['Export Template 1', 'Export Template 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_content_type(self): params = {'content_type': ContentType.objects.get(model='site').pk} @@ -154,8 +154,8 @@ class ConfigContextTestCase(TestCase): c.tenants.set([tenants[i]]) def test_name(self): - params = {'name': 'Config Context 1'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'name': ['Config Context 1', 'Config Context 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_is_active(self): params = {'is_active': True} diff --git a/netbox/extras/tests/test_webhooks.py b/netbox/extras/tests/test_webhooks.py index 026a82bb8..06b4f7c7e 100644 --- a/netbox/extras/tests/test_webhooks.py +++ b/netbox/extras/tests/test_webhooks.py @@ -34,7 +34,7 @@ class WebhookTest(APITestCase): DUMMY_SECRET = "LOOKATMEIMASECRETSTRING" webhooks = Webhook.objects.bulk_create(( - Webhook(name='Site Create Webhook', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers={'X-Foo': 'Bar'}), + Webhook(name='Site Create Webhook', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'), Webhook(name='Site Update Webhook', type_update=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET), Webhook(name='Site Delete Webhook', type_delete=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET), )) diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py index cfa05d0f6..8b20641d7 100644 --- a/netbox/extras/webhooks.py +++ b/netbox/extras/webhooks.py @@ -1,4 +1,3 @@ -import datetime import hashlib import hmac diff --git a/netbox/extras/webhooks_worker.py b/netbox/extras/webhooks_worker.py index e48d8a2d7..1b1b76dd9 100644 --- a/netbox/extras/webhooks_worker.py +++ b/netbox/extras/webhooks_worker.py @@ -1,19 +1,21 @@ -import json +import logging import requests from django_rq import job -from rest_framework.utils.encoders import JSONEncoder +from jinja2.exceptions import TemplateError -from .choices import ObjectChangeActionChoices, WebhookContentTypeChoices +from .choices import ObjectChangeActionChoices from .webhooks import generate_signature +logger = logging.getLogger('netbox.webhooks_worker') + @job('default') def process_webhook(webhook, data, model_name, event, timestamp, username, request_id): """ Make a POST request to the defined Webhook """ - payload = { + context = { 'event': dict(ObjectChangeActionChoices)[event].lower(), 'timestamp': timestamp, 'model': model_name, @@ -21,29 +23,48 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque 'request_id': request_id, 'data': data } + + # Build the headers for the HTTP request headers = { 'Content-Type': webhook.http_content_type, } - if webhook.additional_headers: - headers.update(webhook.additional_headers) + try: + headers.update(webhook.render_headers(context)) + except (TemplateError, ValueError) as e: + logger.error("Error parsing HTTP headers for webhook {}: {}".format(webhook, e)) + raise e + # Render the request body + try: + body = webhook.render_body(context) + except TemplateError as e: + logger.error("Error rendering request body for webhook {}: {}".format(webhook, e)) + raise e + + # Prepare the HTTP request params = { - 'method': 'POST', + 'method': webhook.http_method, 'url': webhook.payload_url, - 'headers': headers + 'headers': headers, + 'data': body, } + logger.info( + "Sending {} request to {} ({} {})".format( + params['method'], params['url'], context['model'], context['event'] + ) + ) + logger.debug(params) + try: + prepared_request = requests.Request(**params).prepare() + except requests.exceptions.RequestException as e: + logger.error("Error forming HTTP request: {}".format(e)) + raise e - if webhook.http_content_type == WebhookContentTypeChoices.CONTENTTYPE_JSON: - params.update({'data': json.dumps(payload, cls=JSONEncoder)}) - elif webhook.http_content_type == WebhookContentTypeChoices.CONTENTTYPE_FORMDATA: - params.update({'data': payload}) - - prepared_request = requests.Request(**params).prepare() - + # If a secret key is defined, sign the request with a hash of the key and its content if webhook.secret != '': - # Sign the request with a hash of the secret key and its content. prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret) + # Send the request with requests.Session() as session: session.verify = webhook.ssl_verification if webhook.ca_file_path: @@ -51,8 +72,10 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque response = session.send(prepared_request) if 200 <= response.status_code <= 299: + logger.info("Request succeeded; response status {}".format(response.status_code)) return 'Status {} returned, webhook successfully processed.'.format(response.status_code) else: + logger.warning("Request failed; response status {}: {}".format(response.status_code, response.content)) raise requests.exceptions.RequestException( "Status {} returned with content '{}', webhook FAILED to process.".format( response.status_code, response.content diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 486a33a2e..1cfc9038d 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -8,7 +8,8 @@ from dcim.models import Device, Interface, Region, Site from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from tenancy.filters import TenancyFilterSet from utilities.filters import ( - MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter, + BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, TagFilter, + TreeNodeMultipleChoiceFilter, ) from virtualization.models import VirtualMachine from .choices import * @@ -28,11 +29,7 @@ __all__ = ( ) -class VRFFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -53,22 +50,14 @@ class VRFFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS fields = ['name', 'rd', 'enforce_unique'] -class RIRFilterSet(NameSlugSearchFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = RIR fields = ['name', 'slug', 'is_private'] -class AggregateFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class AggregateFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -114,11 +103,11 @@ class AggregateFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): try: query = str(netaddr.IPNetwork(value).cidr) return queryset.filter(prefix=query) - except ValidationError: + except (AddrFormatError, ValueError): return queryset.none() -class RoleFilterSet(NameSlugSearchFilterSet): +class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -129,11 +118,7 @@ class RoleFilterSet(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -174,12 +159,14 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -238,7 +225,7 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt try: query = str(netaddr.IPNetwork(value).cidr) return queryset.filter(prefix=query) - except ValidationError: + except (AddrFormatError, ValueError): return queryset.none() def search_within(self, queryset, name, value): @@ -281,11 +268,7 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt return queryset.filter(prefix__net_mask_length=value) -class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -409,15 +392,17 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF return queryset.exclude(interface__isnull=value) -class VLANGroupFilterSet(NameSlugSearchFilterSet): +class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -437,23 +422,21 @@ class VLANGroupFilterSet(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class VLANFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', label='Region (ID)', ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='site__region__in', + field_name='site__region', + lookup_expr='in', to_field_name='slug', label='Region (slug)', ) @@ -508,7 +491,7 @@ class VLANFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter return queryset.filter(qs_filter) -class ServiceFilterSet(CreatedUpdatedFilterSet): +class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 2b7fb2a6b..f9c6fe515 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -276,6 +276,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, + label='VRF', widget=APISelect( api_url="/api/ipam/vrfs/", ) @@ -1381,6 +1382,37 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm): tag = TagFilterField(model) +class ServiceCSVForm(CustomFieldModelCSVForm): + 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.', + } + ) + protocol = CSVChoiceField( + choices=ServiceProtocolChoices, + help_text='IP protocol' + ) + + class Meta: + model = Service + fields = Service.csv_headers + help_texts = { + } + + class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Service.objects.all(), diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 025c4c8af..098dac2f2 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -1021,7 +1021,7 @@ class Service(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) - csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'description'] + csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'port', 'description'] class Meta: ordering = ('protocol', 'port', 'pk') # (protocol, port) may be non-unique diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 8f059c652..4dcb0a6c3 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -385,7 +385,7 @@ class InterfaceIPAddressTable(BaseTable): """ List IP addresses assigned to a specific Interface. """ - address = tables.TemplateColumn(IPADDRESS_ASSIGN_LINK, verbose_name='IP Address') + address = tables.LinkColumn(verbose_name='IP Address') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') status = tables.TemplateColumn(STATUS_LABEL) tenant = tables.TemplateColumn(template_code=TENANT_LINK) diff --git a/netbox/ipam/tests/test_filters.py b/netbox/ipam/tests/test_filters.py index f523b1d20..333b80bc8 100644 --- a/netbox/ipam/tests/test_filters.py +++ b/netbox/ipam/tests/test_filters.py @@ -5,6 +5,7 @@ from ipam.choices import * from ipam.filters import * from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from virtualization.models import Cluster, ClusterType, VirtualMachine +from tenancy.models import Tenant, TenantGroup class VRFTestCase(TestCase): @@ -14,13 +15,27 @@ class VRFTestCase(TestCase): @classmethod def setUpTestData(cls): + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + TenantGroup.objects.bulk_create(tenant_groups) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + vrfs = ( - VRF(name='VRF 1', rd='65000:100', enforce_unique=False), - VRF(name='VRF 2', rd='65000:200', enforce_unique=False), - VRF(name='VRF 3', rd='65000:300', enforce_unique=False), - VRF(name='VRF 4', rd='65000:400', enforce_unique=True), - VRF(name='VRF 5', rd='65000:500', enforce_unique=True), - VRF(name='VRF 6', rd='65000:600', enforce_unique=True), + VRF(name='VRF 1', rd='65000:100', tenant=tenants[0], enforce_unique=False), + VRF(name='VRF 2', rd='65000:200', tenant=tenants[0], enforce_unique=False), + VRF(name='VRF 3', rd='65000:300', tenant=tenants[1], enforce_unique=False), + VRF(name='VRF 4', rd='65000:400', tenant=tenants[1], enforce_unique=True), + VRF(name='VRF 5', rd='65000:500', tenant=tenants[2], enforce_unique=True), + VRF(name='VRF 6', rd='65000:600', tenant=tenants[2], enforce_unique=True), ) VRF.objects.bulk_create(vrfs) @@ -38,10 +53,19 @@ class VRFTestCase(TestCase): params = {'enforce_unique': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) - 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_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) class RIRTestCase(TestCase): @@ -75,11 +99,6 @@ class RIRTestCase(TestCase): params = {'is_private': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) - 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) - class AggregateTestCase(TestCase): queryset = Aggregate.objects.all() @@ -198,15 +217,29 @@ class PrefixTestCase(TestCase): ) Role.objects.bulk_create(roles) + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + TenantGroup.objects.bulk_create(tenant_groups) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + prefixes = ( - Prefix(prefix='10.0.0.0/24', site=None, vrf=None, vlan=None, role=None, is_pool=True), - Prefix(prefix='10.0.1.0/24', site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), - Prefix(prefix='10.0.2.0/24', site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), - Prefix(prefix='10.0.3.0/24', site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), - Prefix(prefix='2001:db8::/64', site=None, vrf=None, vlan=None, role=None, is_pool=True), - Prefix(prefix='2001:db8:0:1::/64', site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), - Prefix(prefix='2001:db8:0:2::/64', site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), - Prefix(prefix='2001:db8:0:3::/64', site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), + Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), + Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), + Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), + Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), + Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), + Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), Prefix(prefix='10.0.0.0/16'), Prefix(prefix='2001:db8::/32'), ) @@ -222,11 +255,6 @@ class PrefixTestCase(TestCase): params = {'is_pool': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) - 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_within(self): params = {'within': '10.0.0.0/16'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) @@ -285,6 +313,20 @@ class PrefixTestCase(TestCase): params = {'status': [PrefixStatusChoices.STATUS_DEPRECATED, PrefixStatusChoices.STATUS_RESERVED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + class IPAddressTestCase(TestCase): queryset = IPAddress.objects.all() @@ -332,18 +374,31 @@ class IPAddressTestCase(TestCase): ) Interface.objects.bulk_create(interfaces) - ipaddresses = ( - IPAddress(address='10.0.0.1/24', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), - IPAddress(address='10.0.0.2/24', vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), - IPAddress(address='10.0.0.3/24', vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), - IPAddress(address='10.0.0.4/24', vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), - IPAddress(address='10.0.0.1/25', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), - IPAddress(address='2001:db8::1/64', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), - IPAddress(address='2001:db8::2/64', vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), - IPAddress(address='2001:db8::3/64', vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), - IPAddress(address='2001:db8::4/64', vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), - IPAddress(address='2001:db8::1/65', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + TenantGroup.objects.bulk_create(tenant_groups) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + + ipaddresses = ( + IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), + IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), + IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), + IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), + IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), + IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), + IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), + IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), + IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), + IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), ) IPAddress.objects.bulk_create(ipaddresses) @@ -355,11 +410,6 @@ class IPAddressTestCase(TestCase): params = {'dns_name': ['ipaddress-a', 'ipaddress-b']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) - 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_parent(self): params = {'parent': '10.0.0.0/24'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) @@ -427,6 +477,20 @@ class IPAddressTestCase(TestCase): params = {'role': [IPAddressRoleChoices.ROLE_SECONDARY, IPAddressRoleChoices.ROLE_VIP]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + class VLANGroupTestCase(TestCase): queryset = VLANGroup.objects.all() @@ -524,13 +588,27 @@ class VLANTestCase(TestCase): ) VLANGroup.objects.bulk_create(groups) + tenant_groups = ( + TenantGroup(name='Tenant group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant group 3', slug='tenant-group-3'), + ) + TenantGroup.objects.bulk_create(tenant_groups) + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]), + Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]), + Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]), + ) + Tenant.objects.bulk_create(tenants) + vlans = ( - VLAN(vid=101, name='VLAN 101', site=sites[0], group=groups[0], role=roles[0], status=VLANStatusChoices.STATUS_ACTIVE), - VLAN(vid=102, name='VLAN 102', site=sites[0], group=groups[0], role=roles[0], status=VLANStatusChoices.STATUS_ACTIVE), - VLAN(vid=201, name='VLAN 201', site=sites[1], group=groups[1], role=roles[1], status=VLANStatusChoices.STATUS_DEPRECATED), - VLAN(vid=202, name='VLAN 202', site=sites[1], group=groups[1], role=roles[1], status=VLANStatusChoices.STATUS_DEPRECATED), - VLAN(vid=301, name='VLAN 301', site=sites[2], group=groups[2], role=roles[2], status=VLANStatusChoices.STATUS_RESERVED), - VLAN(vid=302, name='VLAN 302', site=sites[2], group=groups[2], role=roles[2], status=VLANStatusChoices.STATUS_RESERVED), + VLAN(vid=101, name='VLAN 101', site=sites[0], group=groups[0], role=roles[0], tenant=tenants[0], status=VLANStatusChoices.STATUS_ACTIVE), + VLAN(vid=102, name='VLAN 102', site=sites[0], group=groups[0], role=roles[0], tenant=tenants[0], status=VLANStatusChoices.STATUS_ACTIVE), + VLAN(vid=201, name='VLAN 201', site=sites[1], group=groups[1], role=roles[1], tenant=tenants[1], status=VLANStatusChoices.STATUS_DEPRECATED), + VLAN(vid=202, name='VLAN 202', site=sites[1], group=groups[1], role=roles[1], tenant=tenants[1], status=VLANStatusChoices.STATUS_DEPRECATED), + VLAN(vid=301, name='VLAN 301', site=sites[2], group=groups[2], role=roles[2], tenant=tenants[2], status=VLANStatusChoices.STATUS_RESERVED), + VLAN(vid=302, name='VLAN 302', site=sites[2], group=groups[2], role=roles[2], tenant=tenants[2], status=VLANStatusChoices.STATUS_RESERVED), ) VLAN.objects.bulk_create(vlans) @@ -542,11 +620,6 @@ class VLANTestCase(TestCase): params = {'vid': ['101', '201', '301']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) - 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_region(self): regions = Region.objects.all()[:2] params = {'region_id': [regions[0].pk, regions[1].pk]} @@ -579,6 +652,20 @@ class VLANTestCase(TestCase): params = {'status': [VLANStatusChoices.STATUS_ACTIVE, VLANStatusChoices.STATUS_DEPRECATED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_tenant_group(self): + tenant_groups = TenantGroup.objects.all()[:2] + params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + class ServiceTestCase(TestCase): queryset = Service.objects.all() diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index e168f02a4..aedc44bac 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -333,9 +333,6 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = Service - # Disable inapplicable tests - test_import_objects = None - # TODO: Resolve URL for Service creation test_create_object = None @@ -365,6 +362,13 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'tags': 'Alpha,Bravo,Charlie', } + cls.csv_data = ( + "device,name,protocol,port,description", + "Device 1,Service 1,TCP,1,First service", + "Device 1,Service 2,TCP,2,Second service", + "Device 1,Service 3,UDP,3,Third service", + ) + cls.bulk_edit_data = { 'protocol': ServiceProtocolChoices.PROTOCOL_UDP, 'port': 888, diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 604287f24..f1211473e 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -94,6 +94,7 @@ urlpatterns = [ # Services path('services/', views.ServiceListView.as_view(), name='service_list'), + path('services/import/', views.ServiceBulkImportView.as_view(), name='service_import'), path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'), path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'), path('services//', views.ServiceView.as_view(), name='service'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index faf349222..e8041e8dd 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1015,6 +1015,13 @@ class ServiceCreateView(PermissionRequiredMixin, ObjectEditView): return service.parent.get_absolute_url() +class ServiceBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'ipam.add_service' + model_form = forms.ServiceCSVForm + table = tables.ServiceTable + default_return_url = 'ipam:service_list' + + class ServiceEditView(ServiceCreateView): permission_required = 'ipam.change_service' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index ed3974312..b330c8660 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -75,6 +75,7 @@ DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DEBUG = getattr(configuration, 'DEBUG', False) DEVELOPER = getattr(configuration, 'DEVELOPER', False) +DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs')) EMAIL = getattr(configuration, 'EMAIL', {}) ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False) EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) @@ -516,6 +517,7 @@ REST_FRAMEWORK = { SWAGGER_SETTINGS = { 'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema', 'DEFAULT_FIELD_INSPECTORS': [ + 'utilities.custom_inspectors.JSONFieldInspector', 'utilities.custom_inspectors.NullableBooleanFieldInspector', 'utilities.custom_inspectors.CustomChoiceFieldInspector', 'utilities.custom_inspectors.TagListFieldInspector', @@ -531,7 +533,6 @@ SWAGGER_SETTINGS = { 'drf_yasg.inspectors.StringDefaultFieldInspector', ], 'DEFAULT_FILTER_INSPECTORS': [ - 'utilities.custom_inspectors.IdInFilterInspector', 'drf_yasg.inspectors.CoreAPICompatInspector', ], 'DEFAULT_INFO': 'netbox.urls.openapi_info', diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 2c4d504b2..6444f8a27 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -5,7 +5,7 @@ from django.views.static import serve from drf_yasg import openapi from drf_yasg.views import get_schema_view -from netbox.views import APIRootView, HomeView, SearchView +from netbox.views import APIRootView, HomeView, StaticMediaFailureView, SearchView from users.views import LoginView, LogoutView from .admin import admin_site @@ -63,6 +63,9 @@ _patterns = [ path('admin/', admin_site.urls), path('admin/webhook-backend-status/', include('django_rq.urls')), + # Errors + path('media-failure/', StaticMediaFailureView.as_view(), name='media_failure'), + ] if settings.DEBUG: diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 904dc7375..b0f5b6022 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -1,6 +1,6 @@ from collections import OrderedDict -from django.db.models import Count, F, OuterRef, Subquery +from django.db.models import Count, F from django.shortcuts import render from django.views.generic import View from rest_framework.response import Response @@ -8,7 +8,7 @@ from rest_framework.reverse import reverse from rest_framework.views import APIView from circuits.filters import CircuitFilterSet, ProviderFilterSet -from circuits.models import Circuit, CircuitTermination, Provider +from circuits.models import Circuit, Provider from circuits.tables import CircuitTable, ProviderTable from dcim.filters import ( CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, RackGroupFilterSet, SiteFilterSet, @@ -50,15 +50,7 @@ SEARCH_TYPES = OrderedDict(( 'permission': 'circuits.view_circuit', 'queryset': Circuit.objects.prefetch_related( 'type', 'provider', 'tenant', 'terminations__site' - ).annotate( - # Annotate A/Z terminations - a_side=Subquery( - CircuitTermination.objects.filter(circuit=OuterRef('pk')).filter(term_side='A').values('site__name')[:1] - ), - z_side=Subquery( - CircuitTermination.objects.filter(circuit=OuterRef('pk')).filter(term_side='Z').values('site__name')[:1] - ), - ), + ).annotate_sites(), 'filterset': CircuitFilterSet, 'table': CircuitTable, 'url': 'circuits:circuit_list', @@ -308,6 +300,16 @@ class SearchView(View): }) +class StaticMediaFailureView(View): + """ + Display a user-friendly error message with troubleshooting tips when a static media file fails to load. + """ + def get(self, request): + return render(request, 'media_failure.html', { + 'filename': request.GET.get('filename') + }) + + class APIRootView(APIView): _ignore_model_permissions = True exclude_from_schema = True diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index ec45f43e5..6bff895c8 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -234,6 +234,58 @@ table.report th a { margin-bottom: 0; } +/* Admonition (docs) */ +.admonition { + margin-bottom: 10px; + padding-bottom: 2px; +} +.admonition p { + padding: 0 12px; +} +.admonition pre { + margin: 0 12px 10px; +} +.admonition p.admonition-title { + color: rgb(255, 255, 255); + font-weight: bold; + padding: 4px 12px; +} +.admonition p.admonition-title::before { + content: "\f06a"; + font-family: "FontAwesome"; + margin-right: 4px; +} +/* Admonition - Note */ +.admonition.note { + background-color: rgb(231, 242, 250); +} +.admonition.note .admonition-title { + background-color: rgb(106, 176, 222); +} +.admonition.note .admonition-title::before { + content: "\f05a"; +} +/* Admonition - Warning */ +.admonition.warning { + background-color: rgb(255, 237, 204); +} +.admonition.warning .admonition-title { + background-color: rgb(240, 179, 126); +} +.admonition.warning .admonition-title::before { + content: "\f06a"; +} +/* Admonition - Danger */ +.admonition.danger { + background-color: rgb(253, 243, 242); +} +.admonition.danger .admonition-title { + background-color: rgb(242, 159, 151); +} +.admonition.danger .admonition-title::before { + content: "\f071"; +} + /* AJAX loader */ .loading { position: fixed; diff --git a/netbox/project-static/js/rack_elevations.js b/netbox/project-static/js/rack_elevations.js index 138065e3c..9c58b82e3 100644 --- a/netbox/project-static/js/rack_elevations.js +++ b/netbox/project-static/js/rack_elevations.js @@ -1,14 +1,11 @@ // Toggle the display of device images within an SVG rack elevation $('button.toggle-images').click(function() { var selected = $(this).attr('selected'); - var rack_front = $("#rack_front"); - var rack_rear = $("#rack_rear"); + var rack_elevation = $(".rack_elevation"); if (selected) { - $('.device-image', rack_front.contents()).addClass('hidden'); - $('.device-image', rack_rear.contents()).addClass('hidden'); + $('.device-image', rack_elevation.contents()).addClass('hidden'); } else { - $('.device-image', rack_front.contents()).removeClass('hidden'); - $('.device-image', rack_rear.contents()).removeClass('hidden'); + $('.device-image', rack_elevation.contents()).removeClass('hidden'); } $(this).attr('selected', !selected); $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked'); diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 0c2b01f4d..34869b80f 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -3,7 +3,7 @@ from django.db.models import Q from dcim.models import Device from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter +from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter from .models import Secret, SecretRole @@ -13,18 +13,14 @@ __all__ = ( ) -class SecretRoleFilterSet(NameSlugSearchFilterSet): +class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = SecretRole fields = ['id', 'name', 'slug'] -class SecretFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) +class SecretFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/secrets/tests/test_filters.py b/netbox/secrets/tests/test_filters.py index 16ac3ff45..d240de6f4 100644 --- a/netbox/secrets/tests/test_filters.py +++ b/netbox/secrets/tests/test_filters.py @@ -72,11 +72,6 @@ class SecretTestCase(TestCase): params = {'name': ['Secret 1', 'Secret 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_role(self): roles = SecretRole.objects.all()[:2] params = {'role_id': [roles[0].pk, roles[1].pk]} diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index d92e4b64d..d66326c68 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -206,7 +206,7 @@ class SecretBulkImportView(BulkImportView): master_key = None - def _save_obj(self, obj_form): + def _save_obj(self, obj_form, request): """ Encrypt each object before saving it to the database. """ diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 1b7a9da80..6125e4614 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -4,13 +4,27 @@ {% block title %}Home{% endblock %} - NetBox - - - - - - - + + + + + + + @@ -66,13 +80,20 @@ - - - - - - - + + + + + + +