Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
1033c8677a Release v2.3-beta2 2018-02-06 15:12:31 -05:00
341 changed files with 5095 additions and 17043 deletions

48
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,48 @@
<!--
Before opening a new issue, please search through the existing issues to
see if your topic has already been addressed. Note that you may need to
remove the "is:open" filter from the search bar to include closed issues.
Check the appropriate type for your issue below by placing an x between the
brackets. For assistance with installation issues, or for any other issues
other than those listed below, please raise your topic for discussion on
our mailing list:
https://groups.google.com/forum/#!forum/netbox-discuss
Please note that issues which do not fall under any of the below categories
will be closed. Due to an excessive backlog of feature requests, we are
not currently accepting any proposals which extend NetBox's feature scope.
Do not prepend any sort of tag to your issue's title. An administrator will
review your issue and assign labels as appropriate.
--->
### Issue type
[ ] Feature request <!-- An enhancement of existing functionality -->
[ ] Bug report <!-- Unexpected or erroneous behavior -->
[ ] Documentation <!-- A modification to the documentation -->
<!--
Please describe the environment in which you are running NetBox. (Be sure
to verify that you are running the latest stable release of NetBox before
submitting a bug report.) If you are submitting a bug report and have made
any changes to the code base, please first validate that your bug can be
recreated while running an official release.
-->
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.1.3 -->
<!--
BUG REPORTS must include:
* A list of the steps needed for someone else to reproduce the bug
* A description of the expected and observed behavior
* Any relevant error messages (screenshots may also help)
FEATURE REQUESTS must include:
* A detailed description of the proposed functionality
* A use case for the new feature
* A rough description of any necessary changes to the database schema
* Any relevant third-party libraries which would be needed
-->
### Description

View File

@@ -1,35 +0,0 @@
---
name: 🐛 Bug Report
about: Report a reproducible bug in the current release of NetBox
---
<!--
NOTE: This form is only for reproducible bugs. If you need assistance with
NetBox installation, or if you have a general question, DO NOT open an
issue. Instead, post to our mailing list:
https://groups.google.com/forum/#!forum/netbox-discuss
Please describe the environment in which you are running NetBox. Be sure
that you are running an unmodified instance of the latest stable release
before submitting a bug report.
-->
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.3.6 -->
<!--
Describe in detail the steps that someone else can take to reproduce this
bug using the current stable release of NetBox (or the current beta release
where applicable).
-->
### Steps to Reproduce
<!-- What did you expect to happen? -->
### Expected Behavior
<!-- What happened instead? -->
### Observed Behavior

View File

@@ -1,18 +0,0 @@
---
name: 📖 Documentation Change
about: Suggest an addition or modification to the NetBox documentation
---
<!--
Please indicate the nature of the change by placing an X in one of the
boxes below.
-->
### Change Type
[ ] Addition
[ ] Correction
[ ] Deprecation
[ ] Cleanup (formatting, typos, etc.)
<!-- Describe the proposed change(s). -->
### Proposed Changes

View File

@@ -1,54 +0,0 @@
---
name: ✨ Feature Request
about: Propose a new NetBox feature or enhancement
---
<!--
NOTE: This form is only for proposing specific new features or enhancements.
If you have a general idea or question, please post to our mailing list
instead of opening an issue:
https://groups.google.com/forum/#!forum/netbox-discuss
NOTE: Due to an excessive backlog of feature requests, we are not currently
accepting any proposals which significantly extend NetBox's feature scope.
Please describe the environment in which you are running NetBox. Be sure
that you are running an unmodified instance of the latest stable release
before submitting a bug report.
-->
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.3.6 -->
<!--
Describe in detail the new functionality you are proposing. Include any
specific changes to work flows, data models, or the user interface.
-->
### Proposed Functionality
<!--
Convey an example use case for your proposed feature. Write from the
perspective of a NetBox user who would benefit from the proposed
functionality and describe how.
--->
### Use Case
<!--
Note any changes to the database schema necessary to support the new
feature. For example, does the proposal require adding a new model or
field? (Not all new features require database changes.)
--->
### Database Changes
<!--
List any new dependencies on external libraries or services that this new
feature would introduce. For example, does the proposal require the
installation of a new Python package? (Not all new features introduce new
dependencies.)
-->
### External Dependencies

View File

@@ -1,17 +0,0 @@
---
name: 🏡 Housekeeping
about: A change pertaining to the codebase itself
---
<!--
NOTE: This type of issue should be opened only by those reasonably familiar
with NetBox's code base and interested in contributing to its development.
Describe the proposed change(s) in detail.
-->
### Proposed Changes
<!-- Provide justification for the proposed change(s). -->
### Justification

View File

@@ -6,8 +6,6 @@
be able to accept.
Please indicate the relevant feature request or bug report below.
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
-->
### Fixes:

View File

@@ -9,7 +9,7 @@ python:
- "3.5"
install:
- pip install -r requirements.txt
- pip install pycodestyle
- pip install pep8
before_script:
- psql --version
- psql -U postgres -c 'SELECT version();'

File diff suppressed because it is too large Load Diff

View File

@@ -91,13 +91,11 @@ appropriate labels will be applied for categorization.
## Submitting Pull Requests
* Be sure to open an issue **before** starting work on a pull request, and
discuss your idea with the NetBox maintainers before beginning work. This will
help prevent wasting time on something that might we might not be able to
implement. When suggesting a new feature, also make sure it won't conflict with
any work that's already in progress.
* Any pull request which does _not_ relate to an accepted issue will be closed.
* Be sure to open an issue before starting work on a pull request, and discuss
your idea with the NetBox maintainers before beginning work. This will help
prevent wasting time on something that might we might not be able to implement.
When suggesting a new feature, also make sure it won't conflict with any work
that's already in progress.
* When submitting a pull request, please be sure to work off of the `develop`
branch, rather than `master`. The `develop` branch is used for ongoing

View File

@@ -12,11 +12,11 @@ complete list of requirements, see `requirements.txt`. The code is available [on
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss),
or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode.slack.com/)!
or join us in the #netbox Slack channel on [NetworkToCode](https://slack.networktocode.com/)!
### Build Status
NetBox is built against both Python 2.7 and 3.5. Python 3.5 or higher is strongly recommended.
NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
| | status |
|-------------|------------|
@@ -41,19 +41,3 @@ and run `upgrade.sh`.
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
# Related projects
## Supported SDK
- [pynetbox](https://github.com/digitalocean/pynetbox) Python API client library for Netbox.
## Community SDK
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) A ruby client library for Netbox v2.
## Ansible Inventory
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) Ansible dynamic inventory script for Netbox.

View File

@@ -1,25 +0,0 @@
# django-filter-1.1.0 breaks with Django-2.1
Django>=1.11,<2.1
django-cors-headers
django-debug-toolbar
# django-filter-2.0.0 drops Python 2 support (blocked by #2000)
django-filter==1.1.0
django-mptt
django-tables2
django-taggit
django-taggit-serializer
django-timezone-field
# https://github.com/encode/django-rest-framework/issues/6053
djangorestframework==3.8.1
drf-yasg[validation]
graphviz
Markdown
natsort
ncclient
netaddr
paramiko
Pillow
psycopg2-binary
py-gfm
pycryptodome
xmltodict

View File

@@ -1,9 +0,0 @@
# Change Logging
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object is saved to the database, along with meta data including the current time and the user associated with the change. These records form a running changelog both for each individual object as well as NetBox as a whole (Organization > Changelog).
A serialized representation is included for each object in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant.
When a request is made, a random request ID is generated and attached to any change records resulting from the request. For example, editing multiple objects in bulk will create a change record for each object, and each of those objects will be assigned the same request ID. This makes it easy to identify all the change records associated with a particular request.
Change records are exposed in the API via the read-only endpoint `/api/extras/object-changes/`. They may also be exported in CSV format.

View File

@@ -1,5 +0,0 @@
# Contextual Configuration Data
Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
Devices and Virtual Machines may also have a local config context defined. This local context will always overwrite the rendered config context objects for the Device/VM. This is useful in situations were the device requires a one-off value different from the rest of the environment.

View File

@@ -1,26 +0,0 @@
# Custom Fields
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
However, some users might want to associate with objects attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number pointing to the support ticket that was opened to have it installed. This is certainly a legitimate use for NetBox, but it's perhaps not a common enough need to warrant expanding the internal data schema. Instead, you can create a custom field to hold this data.
Custom fields must be created through the admin UI under Extras > Custom Fields. To create a new custom field, select the object(s) to which you want it to apply, and the type of field it will be. NetBox supports six field types:
* Free-form text (up to 255 characters)
* Integer
* Boolean (true/false)
* Date
* URL
* Selection
Assign the field a name. This should be a simple database-friendly string, e.g. `tps_report`. You may optionally assign the field a human-friendly label (e.g. "TPS report") as well; the label will be displayed on forms. If a description is provided, it will appear beneath the field in a form.
Marking the field as required will require the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields. (The default value has no effect for selection fields.)
When creating a selection field, you should create at least two choices. These choices will be arranged first by weight, with lower weights appearing higher in the list, and then alphabetically.
## Using Custom Fields
When a single object is edited, the form will include any custom fields which have been defined for the object type. These fields are included in the "Custom Fields" panel. On the backend, each custom field value is saved separately from the core object as an independent database call, so it's best to avoid adding too many custom fields per object.
When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.

View File

@@ -1,52 +0,0 @@
# Export Templates
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list.
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:
```
{% for rack in queryset %}
Rack: {{ rack.name }}
Site: {{ rack.site.name }}
Height: {{ rack.u_height }}U
{% endfor %}
```
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
## Example
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
```
{% for device in queryset %}{% if device.status and device.primary_ip %}define host{
use generic-switch
host_name {{ device.name }}
address {{ device.primary_ip.address.ip }}
}
{% endif %}{% endfor %}
```
The generated output will look something like this:
```
define host{
use generic-switch
host_name switch1
address 192.0.2.1
}
define host{
use generic-switch
host_name switch2
address 192.0.2.2
}
define host{
use generic-switch
host_name switch3
address 192.0.2.3
}
```

View File

@@ -1,25 +0,0 @@
# Graphs
NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters:
* **Type:** Site, provider, or interface. This determines in which view the graph will be displayed.
* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
* **Name:** The title to display above the graph.
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
## Examples
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
```
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
```
You can define several graphs to provide multiple contexts when viewing an object. For example:
```
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
```

View File

@@ -1,24 +0,0 @@
# 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 9for 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"
],
...
}
```

View File

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

View File

@@ -1,65 +0,0 @@
# 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.
An optional secret key can be configured for each webhook. This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. This digest can be used by the receiver to authenticate the request's content.
## Installation
If you are upgrading from a previous version of Netbox and want to enable the webhook feature, please follow the directions listed in the sections below.
* [Install Redis server and djano-rq package](../installation/2-netbox/#install-python-packages)
* [Modify configuration to enable webhooks](../installation/2-netbox/#webhooks-configuration)
* [Create supervisord program to run the rqworker process](../installation/3-http-daemon/#supervisord-installation)
## Requests
The webhook POST request is structured as so (assuming `application/json` as the Content-Type):
```no-highlight
{
"event": "created",
"signal_received_timestamp": 1508769597,
"model": "Site"
"data": {
...
}
}
```
`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:
```no-highlight
{
"event": "deleted",
"signal_received_timestamp": 1508781858.544069,
"model": "Site",
"data": {
"asn": None,
"comments": "",
"contact_email": "",
"contact_name": "",
"contact_phone": "",
"count_circuits": 0,
"count_devices": 0,
"count_prefixes": 0,
"count_racks": 0,
"count_vlans": 0,
"custom_fields": {},
"facility": "",
"id": 54,
"name": "test",
"physical_address": "",
"region": None,
"shipping_address": "",
"slug": "test",
"tenant": None
}
}
```
A request is considered successful if the response status code is any one of a list of "good" statuses defined in the [requests library](https://github.com/requests/requests/blob/205755834d34a8a6ecf2b0b5b2e9c3e6a7f4e4b6/requests/models.py#L688), otherwise the request is marked as having failed. The user may manually retry a failed request.
## Backend Status
Django-rq includes a status page in the admin site which can be used to view the result of processed webhooks and manually retry any failed webhooks. Access it from http://netbox.local/admin/webhook-backend-status/.

View File

@@ -1,63 +0,0 @@
# 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
Use the `pg_dump` utility to export the entire database to a file:
```no-highlight
pg_dump netbox > netbox.sql
```
When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
```no-highlight
pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql
```
## Load an Exported Database
!!! warning
This will destroy and replace any existing instance of the database.
```no-highlight
psql -c 'drop database netbox'
psql -c 'create database netbox'
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
If you want to export only the database schema, and not the data itself (e.g. for development reference), do the following:
```no-highlight
pg_dump -s netbox > netbox_schema.sql
```
---
# 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
Execute the following command from the root of the NetBox installation path (typically `/opt/netbox`):
```no-highlight
tar -czf netbox_media.tar.gz netbox/media/
```
## Restore the Media Directory
To extract the saved archive into a new installation, run the following from the installation root:
```no-highlight
tar -xf netbox_media.tar.gz
```

View File

@@ -4,9 +4,6 @@ The NetBox API employs token-based authentication. For convenience, cookie authe
A token is a unique identifier that identifies a user to the API. Each user in NetBox may have one or more tokens which he or she can use to authenticate to the API. To create a token, navigate to the API tokens page at `/user/api-tokens/`.
!!! note
The creation and modification of API tokens can be restricted per user by an administrator. If you don't see an option to create an API token, ask an administrator to grant you access.
Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
By default, a token can be used for all operations available via the API. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.

View File

@@ -5,7 +5,7 @@ Supported HTTP methods:
* `GET`: Retrieve an object or list of objects
* `POST`: Create a new object
* `PUT`: Update an existing object, all mandatory fields must be specified
* `PATCH`: Updates an existing object, only specifying the field to be changed
* `PATCH`: Updates an existing object, only specifiying the field to be changed
* `DELETE`: Delete an existing object
To authenticate a request, attach your token in an `Authorization` header:
@@ -144,4 +144,4 @@ $ curl -v -X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f
* Closing connection 0
```
The response to a successful `DELETE` request will have code 204 (No Content); the body of the response will be empty.
The response to a successfull `DELETE` request will have code 204 (No Content); the body of the response will be empty.

View File

@@ -206,28 +206,3 @@ 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
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`):
```
GET /api/ipam/prefixes/?status=1
```
The same filter can be incldued multiple times. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
```
GET /api/ipam/prefixes/?status=1&status=2
```
## 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:
```
GET /api/dcim/sites/?cf_foo=123
```
!!! note
Full versus partial matching when filtering is configurable per custom field. Filtering can be toggled (or disabled) for a custom field in the admin UI.

View File

@@ -1,16 +0,0 @@
# NetBox Configuration
NetBox's local configuration is stored in `netbox/netbox/configuration.py`. An example configuration is provided at `netbox/netbox/configuration.example.py`. You may copy or rename the example configuration and make changes as appropriate. NetBox will not run without a configuration file.
While NetBox has many configuration settings, only a few of them must be defined at the time of installation.
* [Required settings](required-settings.md)
* [Optional settings](optional-settings.md)
## Changing the Configuration
Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect:
```no-highlight
# sudo supervisorctl restart netbox
```

View File

@@ -1,4 +1,4 @@
# Required Configuration Settings
NetBox's local configuration is held in `netbox/netbox/configuration.py`. An example configuration is provided at `netbox/netbox/configuration.example.py`. You may copy or rename the example configuration and make changes as appropriate. NetBox will not run without a configuration file.
## ALLOWED_HOSTS

View File

@@ -1,4 +1,4 @@
# Optional Configuration Settings
The following are optional settings which may be declared in `netbox/netbox/configuration.py`.
## ADMINS
@@ -44,14 +44,6 @@ BASE_PATH = 'netbox/'
---
## CHANGELOG_RETENTION
Default: 90
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain changes in the database indefinitely. (Warning: This will greatly increase database size over time.)
---
## CORS_ORIGIN_ALLOW_ALL
Default: False
@@ -231,14 +223,6 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
---
## WEBHOOKS_ENABLED
Default: False
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../miscellaneous/webhooks/) for more information on setup and use.
---
## Date and Time Formatting
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date).
@@ -253,49 +237,3 @@ SHORT_TIME_FORMAT = 'H:i:s' # 13:23:00
DATETIME_FORMAT = 'N j, Y g:i a' # June 26, 2016 1:23 p.m.
SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-27 13:23
```
---
## Redis Connection Settings
[Redis](https://redis.io/) is a key-value store which functions as a very lightweight database. It is required when enabling NetBox [webhooks](../miscellaneous/webhooks/). A Redis connection is configured using a dictionary similar to the following:
```
REDIS = {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
}
```
### DATABASE
Default: 0
The Redis database ID.
### DEFAULT_TIMEOUT
Default: 300
The timeout value to use when connecting to the Redis server (in seconds).
### HOST
Default: localhost
The hostname or IP address of the Redis server.
### PORT
Default: 6379
The TCP port to use when connecting to the Redis server.
### PASSWORD
Default: None
The password to use when authenticating to the Redis server (optional).

View File

@@ -1,120 +0,0 @@
# 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.)
The device type model includes three flags which inform what type of components may be added to it:
* `is_console_server`: This device type has console server ports
* `is_pdu`: This device type has power outlets
* `is_network_device`: This device type has network interfaces
Some devices house child devices which share physical resources, like space and power, but which functional independently from one another. A common example of this is blade server chassis. Each device type is designated as one of the following:
* A parent device (which has device bays)
* 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.
## 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
* 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.
---
# 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 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.
---
# Device Components
There are six types of device components which comprise all of the interconnection logic with NetBox:
* Console ports
* Console server ports
* Power ports
* Power outlets
* Network interfaces
* 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 form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor 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.)
## Device Bays
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
---
# 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.
---
# Inventory Items
Inventory items represent hardware components installed within a device, such as a power supply or CPU. 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.

View File

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

View File

@@ -1,5 +0,0 @@
# 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.)

View File

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

View File

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

View File

@@ -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)

View File

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

View File

@@ -1,16 +1,18 @@
The circuits component of NetBox deals with the management of long-haul Internet and private transit links and providers.
# 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.
Each provider may be assigned an autonomous system number (ASN), an account number, and contact information.
---
# 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.
A circuit represents a single physical data link connecting two endpoints. Each circuit belongs to a provider and must be assigned a circuit ID which is unique to that provider.
## Circuit Types
### Circuit Types
Circuits are classified by type. For example, you might define circuit types for:
@@ -21,7 +23,7 @@ Circuits are classified by type. For example, you might define circuit types for
Circuit types are fully customizable.
## Circuit Terminations
### 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.
@@ -29,6 +31,3 @@ Each circuit termination is tied to a site, and optionally to a specific device
!!! 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.

122
docs/data-model/dcim.md Normal file
View File

@@ -0,0 +1,122 @@
Data center infrastructure management (DCIM) entails all physical assets: sites, racks, devices, cabling, etc.
# 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.
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment, and an Autonomous System (AS) number.
### 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.
---
# Racks
The rack model represents a physical two- or four-post equipment rack in which equipment is mounted. Each rack is assigned to a site. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U, but NetBox allows you to define racks of arbitrary height. Each rack has two faces (front and rear) on which devices can be mounted.
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." The facility ID can alternatively be used to store a rack's serial number.
The available rack types include 2- and 4-post frames, 4-post cabinet, and wall-mounted frame and cabinet. 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 group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not supported.
### 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).
---
# Device Types
A device type represents a particular hardware model that exists in the real world. Device types describe the physical attributes of a device (rack height and depth), its class (e.g. console server, PDU, etc.), and its individual components (console, power, and data).
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.
### Manufacturers
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. 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 interfaces a device has. These are:
* Console ports
* Console server ports
* Power ports
* Power outlets
* Interfaces
* Device bays
Whenever a new device is created, it is automatically assigned components per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates:
* 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 of existing devices individually.
---
# 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 depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
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 shows as being 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 air flow.
### Roles
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, a device can belong to only one role.
### Platforms
A device's platform is used to denote the type of software running on it. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
### Inventory Items
Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Each item can optionally be assigned a manufacturer.
!!! note
Prior to version 2.0, inventory items were called modules.
### Components
There are six types of device components which comprise all of the interconnection logic with NetBox:
* Console ports
* Console server ports
* Power ports
* Power outlets
* Interfaces
* Device bays
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.) Each type of connection can be classified as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed.
Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned. Each interface can also be designated as management-only (for out-of-band management) and assigned a short description.
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 on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
---
# Virtual Chassis
A virtual chassis represents a set of devices which share a single control plane: for example, a stack of switches which are managed as a single device. 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.

132
docs/data-model/extras.md Normal file
View File

@@ -0,0 +1,132 @@
This section entails features of NetBox which are not crucial to its primary functions, but provide additional value.
# Custom Fields
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
However, some users might want to associate with objects attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number pointing to the support ticket that was opened to have it installed. This is certainly a legitimate use for NetBox, but it's perhaps not a common enough need to warrant expanding the internal data schema. Instead, you can create a custom field to hold this data.
Custom fields must be created through the admin UI under Extras > Custom Fields. To create a new custom field, select the object(s) to which you want it to apply, and the type of field it will be. NetBox supports six field types:
* Free-form text (up to 255 characters)
* Integer
* Boolean (true/false)
* Date
* URL
* Selection
Assign the field a name. This should be a simple database-friendly string, e.g. `tps_report`. You may optionally assign the field a human-friendly label (e.g. "TPS report") as well; the label will be displayed on forms. If a description is provided, it will appear beneath the field in a form.
Marking the field as required will require the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields. (The default value has no effect for selection fields.)
When creating a selection field, you should create at least two choices. These choices will be arranged first by weight, with lower weights appearing higher in the list, and then alphabetically.
## Using Custom Fields
When a single object is edited, the form will include any custom fields which have been defined for the object type. These fields are included in the "Custom Fields" panel. On the backend, each custom field value is saved separately from the core object as an independent database call, so it's best to avoid adding too many custom fields per object.
When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.
# Export Templates
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list.
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:
```
{% for rack in queryset %}
Rack: {{ rack.name }}
Site: {{ rack.site.name }}
Height: {{ rack.u_height }}U
{% endfor %}
```
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
## Example
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
```
{% for device in queryset %}{% if device.status and device.primary_ip %}define host{
use generic-switch
host_name {{ device.name }}
address {{ device.primary_ip.address.ip }}
}
{% endif %}{% endfor %}
```
The generated output will look something like this:
```
define host{
use generic-switch
host_name switch1
address 192.0.2.1
}
define host{
use generic-switch
host_name switch2
address 192.0.2.2
}
define host{
use generic-switch
host_name switch3
address 192.0.2.3
}
```
# Graphs
NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters:
* **Type:** Site, provider, or interface. This determines in which view the graph will be displayed.
* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
* **Name:** The title to display above the graph.
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
## Examples
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
```
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
```
You can define several graphs to provide multiple contexts when viewing an object. For example:
```
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
```
# Topology Maps
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure).
To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
Each line of the **device patterns** field represents a hierarchical layer within the topology map. For example, you might map a traditional network with core, distribution, and access tiers like this:
```
core-switch-[abcd]
dist-switch\d
access-switch\d+;oob-switch\d+
```
Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.
# Image Attachments
Certain objects within NetBox (namely sites, racks, and devices) can have photos or other images attached to them. (Note that _only_ image files are supported.) Each attachment may optionally be assigned a name; if omitted, the attachment will be represented by its file name.
!!! note
If you experience a server error while attempting to upload an image attachment, verify that the system user NetBox runs as has write permission to the media root directory (`netbox/media/`).

99
docs/data-model/ipam.md Normal file
View File

@@ -0,0 +1,99 @@
IP address management (IPAM) entails the allocation of IP networks, addresses, and related numeric resources.
# VRFs
A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain within a network. Each VRF is essentially a separate routing table: the same IP prefix or address can exist in multiple VRFs. VRFs are commonly used to isolate customers or organizations from one another within a network.
Each VRF is assigned a name and a unique route distinguisher (RD). VRFs are an optional feature of NetBox: Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
!!! note
By default, NetBox allows for overlapping IP space both in the global table and within each VRF. Unique space enforcement can be toggled per-VRF as well as in the global table using the `ENFORCE_GLOBAL_UNIQUE` configuration setting.
---
# Aggregates
IP address space is organized as a hierarchy, with more-specific (smaller) prefixes arranged as child nodes under less-specific (larger) prefixes. For example:
* 10.0.0.0/8
* 10.1.0.0/16
* 10.1.2.0/24
The root of the IPv4 hierarchy is 0.0.0.0/0, which encompasses all possible IPv4 addresses (and similarly, ::/0 for IPv6). However, even the largest organizations use only a small fraction of the global address space. Therefore, it makes sense to track in NetBox only the address space which is of interest to your organization.
Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the private IPv4 space set aside in RFC 1918. So, you might define three aggregates for this space:
* 10.0.0.0/8
* 172.16.0.0/12
* 192.168.0.0/16
Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space. (Most organizations will not have a need to track IPv6 link local space.)
Prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation. Total utilization for each aggregate is displayed in the aggregates list.
Aggregates cannot overlap with one another; they can only exist in parallel. 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.
### RIRs
Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, 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.
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). Each RIR can be annotated as representing only private space.
---
# 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.
Each prefix may be assigned to one VRF; prefixes not assigned to a VRF are assigned to the "global" table. Prefixes are also organized under their respective aggregates, irrespective of VRF assignment.
A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. Each prefix may also be assigned a short description.
### Statuses
Each prefix is assigned an operational status. This is 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
### Roles
Whereas a status describes a prefix's operational state, a role describes its function. For example, roles might include:
* Access segment
* Infrastructure
* NAT
* Lab
* Out-of-band
Role assignment is optional and roles are fully customizable.
---
# IP Addresses
An IP address comprises a single 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 (or it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs. Each IP address can also be assigned a short description.
An IP address can be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address (for both IPv4 and IPv6).
One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily to denote the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not supported.
---
# 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. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role, and may include a short description.
### VLAN Groups
VLAN groups can be employed for administrative organization within NetBox. 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 within a site.
---
# Services
A service represents a TCP or UDP service available on a device or virtual machine. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to its parent. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)

View File

@@ -1,12 +1,14 @@
"Secrets" are small amounts of data that must be kept confidential; for example, passwords and SNMP community strings. NetBox provides encrypted storage of secret data.
# 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.
A secret represents a single credential or other string 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
### Roles
Each secret is assigned a functional role which indicates what it is used for. Secret roles are customizable. Typical roles might include:
Each secret is assigned a functional role which indicates what it is used for. Typical roles might include:
* Login credentials
* SNMP community strings

View File

@@ -0,0 +1,20 @@
NetBox supports the assignment of resources to tenant organizations. Typically, these are used to represent individual customers of or internal departments within the organization using NetBox.
# Tenants
A tenant represents a discrete organization. The following objects can be assigned to tenants:
* Sites
* Racks
* Devices
* VRFs
* Prefixes
* IP addresses
* VLANs
* Circuits
If a prefix or IP address is not assigned to a tenant, it will appear to inherit the tenant to which its parent VRF is assigned, if any.
### Tenant Groups
Tenants can be grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional.

View File

@@ -0,0 +1,29 @@
NetBox supports the definition of virtual machines arranged in clusters. A cluster can optionally have physical host devices associated with it.
# 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 an organizational group.
Physical devices (from NetBox's DCIM component) 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.
---
# 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 have interfaces created on it. These behave similarly to device interfaces, and can be assigned IP addresses, however given their virtual nature they cannot be connected to other interfaces. VMs can also be assigned layer four services. Unlike physical devices, VMs cannot be assigned console or power ports, or device bays.
The following resources can be defined for each VM:
* vCPU count
* Memory (MB)
* Disk space (GB)

View File

@@ -1,70 +0,0 @@
# Extending Models
Below is a list of items to consider when adding a new field to a model:
### 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.
```
./manage.py makemigrations <app> -n <name>
./manage.py migrate
```
Where possible, try to merge related changes into a single migration. For example, if three new fields are being added to different models within an app, these can be expressed in the same migration. You can merge a new migration with an existing one by combining their `operations` lists.
!!! 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()`
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:
```
class Foo(models.Model):
def clean(self):
super(DeviceCSVForm, self).clean()
# Custom validation goes here
if self.bar is None:
raise ValidationError()
```
### 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
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 `select_related()` or `prefetch_related()` as appropriate. This will optimize the view and avoid excessive database lookups.
### 5. Update API serializer
Extend the model's API serializer in `<app>.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 field to forms
Extend any forms to include the new field as appropriate. Common forms include:
* **Credit/edit** - Manipulating a single object
* **Bulk edit** - Performing a change on mnay objects at once
* **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)
### 7. 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.
### 8. 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.
### 9. 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.
### 10. Adjust API and model tests
Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields.

View File

@@ -1,30 +0,0 @@
# NetBox Development
NetBox is maintained as a [GitHub project](https://github.com/digitalocean/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
## Communication
Communication among developers should always occur via public channels:
* [GitHub issues](https://github.com/digitalocean/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
* [The mailing list](https://groups.google.com/forum/#!forum/netbox-discuss) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
* [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
## Governance
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions (in other words, avoid scope creep).
## Project Structure
All development of the current NetBox release occurs in the `develop` branch; releases are packaged from the `master` branch. The `master` branch should _always_ represent the current stable release in its entirety, such that installing NetBox by either downloading a packaged release or cloning the `master` branch provides the same code base.
NetBox components are arranged into functional subsections called _apps_ (a carryover from Django verancular). Each app holds the models, views, and templates relevant to a particular function:
* `circuits`: Communications circuits and providers (not to be confused with power circuits)
* `dcim`: Datacenter infrastructure management (sites, racks, and devices)
* `extras`: Additional features not considered part of the core data model
* `ipam`: IP address management (VRFs, prefixes, IP addresses, and VLANs)
* `secrets`: Encrypted storage of sensitive data (e.g. login credentials)
* `tenancy`: Tenants (such as customers) to which NetBox objects may be assigned
* `utilities`: Resources which are not user-facing (extendable classes, etc.)
* `virtualization`: Virtual machines and clusters

View File

@@ -1,81 +0,0 @@
# 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:
```
# https://github.com/encode/django-rest-framework/issues/6053
djangorestframework==3.8.1
```
The other file is `requirements.txt`, which lists each of the required packages pinned to its current stable version. When NetBox is installed, the Python environment is configured to match this file. This helps ensure that a new release of a dependency doesn't break NetBox.
Every minor version release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this:
1. Create a new virtual environment.
2. Install the latest version of all required packages via pip:
```
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 the following static libraries to their most recent stable release:
* Bootstrap 3
* Font Awesome 4
* jQuery
* jQuery UI
## 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 on GitHub. Ensure that there are no remaining open issues associated with it.
---
# All Releases
## Verify CI Build Status
Ensure that continuous integration testing on the `develop` branch is completing successfully.
## Update Version and Changelog
Update the `VERSION` constant in `settings.py` to the new release version and add the current date to the release notes in `CHANGELOG.md`.
## Submit a Pull Request
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.
Once CI has completed on the PR, merge it.
## Create a New Release
Draft a [new release](https://github.com/digitalocean/netbox/releases/new) with the following parameters.
* **Tag:** Current version (e.g. `v2.3.4`)
* **Target:** `master`
* **Title:** Version and date (e.g. `v2.3.4 - 2018-08-02`)
Copy the description from the pull request into the release notes.
## 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:
```
VERSION = 'v2.3.5-dev'
```
## 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.

View File

@@ -1,41 +0,0 @@
# Style Guide
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh`.
## PEP 8 Exceptions
* Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
* The library being import contains only constant declarations (`constants.py`)
* The library being imported explicitly defines `__all__` (e.g. `<app>.api.nested_serializers`)
* Maximum line length is 120 characters (E501)
* This does not apply to HTML templates or to automatically generated code (e.g. database migrations).
* Line breaks are permitted following binary operators (W504)
## Enforcing Code Style
The `pycodestyle` utility (previously `pep8`) is used by the CI process to enforce code style. It is strongly recommended to include as part of your commit process. A git commit hook is provided in the source at `scripts/git-hooks/pre-commit`. Linking to this script from `.git/hooks/` will invoke `pycodestyle` prior to every commit attempt and abort if the validation fails.
```
$ cd .git/hooks/
$ ln -s ../../scripts/git-hooks/pre-commit
```
To invoke `pycodestyle` manually, run:
```
pycodestyle --ignore=W504,E501 netbox/
```
## General Guidance
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point.
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely.
* Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
* Every model should have a docstring. Every custom method should include an expalantion of its function.
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.

View File

@@ -1,5 +1,3 @@
![NetBox](netbox_logo.png "NetBox logo")
# What is NetBox?
NetBox is an open source web application designed to help manage and document computer networks. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. It encompasses the following aspects of network management:
@@ -12,7 +10,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 Isn't
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:
@@ -44,15 +42,13 @@ When given a choice between a relatively simple [80% solution](https://en.wikipe
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.
| Function | Component |
|--------------------|-------------------|
| HTTP service | nginx or Apache |
| WSGI service | gunicorn or uWSGI |
| Application | Django/Python |
| Database | PostgreSQL 9.4+ |
| Task queuing | Redis/django-rq |
| Live device access | NAPALM |
| Function | Component |
|--------------|-------------------|
| HTTP Service | nginx or Apache |
| WSGI Service | gunicorn or uWSGI |
| Application | Django/Python |
| Database | PostgreSQL 9.4+ |
# Getting Started
See the [installation guide](installation/index.md) for help getting NetBox up and running quickly.
See the [installation guide](installation/postgresql.md) for help getting NetBox up and running quickly.

View File

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

View File

@@ -7,13 +7,13 @@ This guide explains how to implement LDAP authentication using an external serve
On Ubuntu:
```no-highlight
sudo apt-get install -y libldap2-dev libsasl2-dev libssl-dev
sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev
```
On CentOS:
```no-highlight
sudo yum install -y openldap-devel
sudo yum install -y python-devel openldap-devel
```
## Install django-auth-ldap
@@ -81,13 +81,13 @@ AUTH_LDAP_USER_ATTR_MAP = {
# 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` .
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
```python
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group
# hierarchy.
# heirarchy.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("dc=example,dc=com", ldap.SCOPE_SUBTREE,
"(objectClass=group)")
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()

View File

@@ -1,38 +1,33 @@
# Migration
!!! warning
Beginning with v2.5, NetBox will no longer support Python 2. It is strongly recommended that you upgrade to Python 3 as soon as possible.
## Ubuntu
Remove the Python2 version of gunicorn:
Remove Python 2 packages
```no-highlight
# pip uninstall -y gunicorn
# apt-get remove --purge -y python-dev python-pip
```
Install Python3 and pip3, Python's package management tool:
Install Python 3 packages
```no-highlight
# apt-get update
# apt-get install -y python3 python3-dev python3-setuptools
# easy_install3 pip
# apt-get install -y python3 python3-dev python3-pip
```
Install the Python3 packages required by NetBox:
Install Python Packages
```no-highlight
# cd /opt/netbox
# pip3 install -r requirements.txt
```
Replace gunicorn with the Python3 version:
Gunicorn Update
```no-highlight
# pip uninstall gunicorn
# pip3 install gunicorn
```
If using LDAP authentication, install the `django-auth-ldap` package:
Re-install LDAP Module (optional if using LDAP for auth)
```no-highlight
# pip3 install django-auth-ldap
sudo pip3 install django-auth-ldap
```

View File

@@ -2,21 +2,43 @@
This section of the documentation discusses installing and configuring the NetBox application.
!!! note
Python 3 is strongly encouraged for new installations. Support for Python 2 will be discontinued in the near future. This documentation includes a guide on [migrating from Python 2 to Python 3](migrating-to-python3).
**Ubuntu**
Python 3:
```no-highlight
# apt-get install -y python3 python3-dev python3-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install3 pip
```
Python 2:
```no-highlight
# apt-get install -y python2.7 python-dev python-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install pip
```
**CentOS**
Python 3:
```no-highlight
# yum install -y epel-release
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install-3.4 pip
```
Python 2:
```no-highlight
# yum install -y epel-release
# yum install -y gcc python2 python-devel python-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install pip
```
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
@@ -69,49 +91,37 @@ 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.)
`# chown -R netbox:netbox /opt/netbox/netbox/media/`
```
# chown -R netbox:netbox /opt/netbox/netbox/media/
```
# Install Python Packages
## Install Python Packages
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
Python 3:
```no-highlight
# pip3 install -r requirements.txt
```
Python 2:
```no-highlight
# pip install -r requirements.txt
```
!!! 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`.
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 `pip -V` or `pip3 -V`.
## NAPALM Automation (Optional)
### NAPALM Automation
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:
As of v2.1.0, 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
```
## Webhooks (Optional)
[Webhooks](../data-model/extras/#webhooks) allow NetBox to integrate with external services by pushing out a notification each time a relevant object is created, updated, or deleted. Enabling the webhooks feature requires [Redis](https://redis.io/), a lightweight in-memory database. You may opt to install a Redis sevice locally (see below) or connect to an external one.
**Ubuntu**
```no-highlight
# apt-get install -y redis-server
```
**CentOS**
```no-highlight
# yum install -y redis
```
Enabling webhooks also requires installing the [`django-rq`](https://github.com/ui/django-rq) package. This allows NetBox to use the Redis database as a queue for outgoing webhooks.
```no-highlight
# pip3 install django-rq
```
# Configuration
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
@@ -162,23 +172,11 @@ 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.
## Webhooks Configuration
If you have opted to enable the webhooks, set `WEBHOOKS_ENABLED = True` and define the relevant `REDIS` database parameters. Below is an example:
```python
WEBHOOKS_ENABLED = True
REDIS = {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
}
```
# Run Database Migrations
!!! warning
The examples on the rest of this page call the `python3` executable. Replace this with `python2` or `python` if you're using Python 2.
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

View File

@@ -12,37 +12,25 @@ Download and extract the latest version:
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/
# ln -sfn netbox-X.Y.Z/ netbox
# ln -sf netbox-X.Y.Z/ netbox
```
Copy the 'configuration.py' you created when first installing to the new version:
```no-highlight
# cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.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.
```no-highlight
# cp -r /opt/netbox-X.Y.X/netbox/reports /opt/netbox/netbox/reports/
# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py
```
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 /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.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
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py
```
## Option B: Clone the Git Repository (latest master release)
@@ -65,7 +53,7 @@ Once the new code is in place, run the upgrade script (which may need to be run
```
!!! warning
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below. Note that Python 2 will no longer be supported in NetBox v2.5.
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below.
```no-highlight
# ./upgrade.sh -2
@@ -92,9 +80,3 @@ Finally, restart the WSGI service to run the new code. If you followed this guid
```no-highlight
# sudo supervisorctl restart netbox
```
If using webhooks, also restart the Redis worker:
```no-highlight
# sudo supervisorctl restart netbox-rqworker
```

View File

@@ -56,7 +56,7 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
## Option B: Apache
```no-highlight
# apt-get install -y apache2 libapache2-mod-wsgi-py3
# apt-get install -y apache2
```
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
@@ -82,7 +82,6 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
ProxyPass !
</Location>
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
ProxyPass / http://127.0.0.1:8001/
ProxyPassReverse / http://127.0.0.1:8001/
</VirtualHost>
@@ -93,7 +92,6 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
```no-highlight
# a2enmod proxy
# a2enmod proxy_http
# a2enmod headers
# a2ensite netbox
# service apache2 restart
```
@@ -102,7 +100,7 @@ To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https
# gunicorn Installation
Install gunicorn:
Install gunicorn using `pip3` (Python 3) or `pip` (Python 2):
```no-highlight
# pip3 install gunicorn
@@ -133,11 +131,6 @@ Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command`
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
directory = /opt/netbox/netbox/
user = www-data
[program:netbox-rqworker]
command = python3 /opt/netbox/netbox/manage.py rqworker
directory = /opt/netbox/netbox/
user = www-data
```
Then, restart the supervisor service to detect and run the gunicorn service:

View File

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

View File

@@ -1,4 +1,4 @@
NetBox includes a Python shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
NetBox includes a Python shell withing which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
```
./manage.py nbshell
@@ -86,7 +86,7 @@ The `count()` method can be appended to the queryset to return a count of object
982
```
Relationships with other models can be traversed by concatenating field names with a double-underscore. For example, the following will return all devices assigned to the tenant named "Pied Piper."
Relationships with other models can be traversed by concatenting field names with a double-underscore. For example, the following will return all devices assigned to the tenant named "Pied Piper."
```
>>> Device.objects.filter(tenant__name='Pied Piper')

View File

@@ -1,54 +1,35 @@
site_name: NetBox
theme: readthedocs
repo_url: https://github.com/digitalocean/netbox
pages:
- 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'
- Upgrading NetBox: 'installation/upgrading.md'
- Migrating to Python3: 'installation/migrating-to-python3.md'
- Configuration:
- Configuring NetBox: 'configuration/index.md'
- Required Settings: 'configuration/required-settings.md'
- Optional Settings: 'configuration/optional-settings.md'
- Core Functionality:
- IP Address Management: 'core-functionality/ipam.md'
- VLANs: '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'
- Circuits: 'core-functionality/circuits.md'
- Secrets: 'core-functionality/secrets.md'
- Tenancy: 'core-functionality/tenancy.md'
- Additional Features:
- Tags: 'additional-features/tags.md'
- Custom Fields: 'additional-features/custom-fields.md'
- Context Data: 'additional-features/context-data.md'
- Export Templates: 'additional-features/export-templates.md'
- Graphs: 'additional-features/graphs.md'
- Topology Maps: 'additional-features/topology-maps.md'
- Reports: 'additional-features/reports.md'
- Webhooks: 'additional-features/webhooks.md'
- Change Logging: 'additional-features/change-logging.md'
- Administration:
- Replicating NetBox: 'administration/replicating-netbox.md'
- API:
- Overview: 'api/overview.md'
- Authentication: 'api/authentication.md'
- Working with Secrets: 'api/working-with-secrets.md'
- Examples: 'api/examples.md'
- Development:
- Introduction: 'development/index.md'
- Style Guide: 'development/style-guide.md'
- Utility Views: 'development/utility-views.md'
- Extending Models: 'development/extending-models.md'
- Release Checklist: 'development/release-checklist.md'
- 'Introduction': 'index.md'
- 'Installation':
- 'PostgreSQL': 'installation/postgresql.md'
- 'NetBox': 'installation/netbox.md'
- 'Web Server': 'installation/web-server.md'
- 'LDAP (Optional)': 'installation/ldap.md'
- 'Upgrading': 'installation/upgrading.md'
- 'Migrating to Python3': 'installation/migrating-to-python3.md'
- 'Configuration':
- 'Mandatory Settings': 'configuration/mandatory-settings.md'
- 'Optional Settings': 'configuration/optional-settings.md'
- 'Data Model':
- 'Circuits': 'data-model/circuits.md'
- 'DCIM': 'data-model/dcim.md'
- 'IPAM': 'data-model/ipam.md'
- 'Secrets': 'data-model/secrets.md'
- 'Tenancy': 'data-model/tenancy.md'
- 'Virtualization': 'data-model/virtualization.md'
- 'Extras': 'data-model/extras.md'
- 'API':
- 'Overview': 'api/overview.md'
- 'Authentication': 'api/authentication.md'
- 'Working with Secrets': 'api/working-with-secrets.md'
- 'Examples': 'api/examples.md'
- 'Miscellaneous':
- 'Reports': 'miscellaneous/reports.md'
- 'Shell': 'miscellaneous/shell.md'
- 'Development':
- 'Utility Views': 'development/utility-views.md'
markdown_extensions:
- admonition:

View File

@@ -1,32 +1,30 @@
from __future__ import unicode_literals
from rest_framework import serializers
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from circuits.constants import CIRCUIT_STATUS_CHOICES
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.api.serializers import NestedInterfaceSerializer, NestedSiteSerializer
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
#
# Providers
#
class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
tags = TagListSerializerField(required=False)
class ProviderSerializer(CustomFieldModelSerializer):
class Meta:
model = Provider
fields = [
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
'custom_fields', 'created', 'last_updated',
]
class NestedProviderSerializer(WritableNestedSerializer):
class NestedProviderSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
class Meta:
@@ -34,6 +32,16 @@ class NestedProviderSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'name', 'slug']
class WritableProviderSerializer(CustomFieldModelSerializer):
class Meta:
model = Provider
fields = [
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
'custom_fields', 'created', 'last_updated',
]
#
# Circuit types
#
@@ -45,7 +53,7 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug']
class NestedCircuitTypeSerializer(WritableNestedSerializer):
class NestedCircuitTypeSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
class Meta:
@@ -57,22 +65,21 @@ class NestedCircuitTypeSerializer(WritableNestedSerializer):
# Circuits
#
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
class CircuitSerializer(CustomFieldModelSerializer):
provider = NestedProviderSerializer()
status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES)
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False)
tenant = NestedTenantSerializer()
class Meta:
model = Circuit
fields = [
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'comments', 'custom_fields', 'created', 'last_updated',
]
class NestedCircuitSerializer(WritableNestedSerializer):
class NestedCircuitSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
@@ -80,14 +87,33 @@ class NestedCircuitSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'cid']
class WritableCircuitSerializer(CustomFieldModelSerializer):
class Meta:
model = Circuit
fields = [
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
'comments', 'custom_fields', 'created', 'last_updated',
]
#
# Circuit Terminations
#
class CircuitTerminationSerializer(ValidatedModelSerializer):
class CircuitTerminationSerializer(serializers.ModelSerializer):
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer()
interface = NestedInterfaceSerializer(required=False, allow_null=True)
interface = InterfaceSerializer()
class Meta:
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
]
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
class Meta:
model = CircuitTermination

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from circuits import filters
@@ -19,7 +19,6 @@ from . import serializers
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Circuit, ['status']),
(CircuitTermination, ['term_side']),
)
@@ -29,11 +28,12 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
#
class ProviderViewSet(CustomFieldModelViewSet):
queryset = Provider.objects.prefetch_related('tags')
queryset = Provider.objects.all()
serializer_class = serializers.ProviderSerializer
write_serializer_class = serializers.WritableProviderSerializer
filter_class = filters.ProviderFilter
@action(detail=True)
@detail_route()
def graphs(self, request, pk=None):
"""
A convenience method for rendering graphs for a particular provider.
@@ -59,8 +59,9 @@ class CircuitTypeViewSet(ModelViewSet):
#
class CircuitViewSet(CustomFieldModelViewSet):
queryset = Circuit.objects.select_related('type', 'tenant', 'provider').prefetch_related('tags')
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
serializer_class = serializers.CircuitSerializer
write_serializer_class = serializers.WritableCircuitSerializer
filter_class = filters.CircuitFilter
@@ -71,4 +72,5 @@ class CircuitViewSet(CustomFieldModelViewSet):
class CircuitTerminationViewSet(ModelViewSet):
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
serializer_class = serializers.CircuitTerminationSerializer
write_serializer_class = serializers.WritableCircuitTerminationSerializer
filter_class = filters.CircuitTerminationFilter

View File

@@ -6,7 +6,7 @@ from django.db.models import Q
from dcim.models import Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NumericInFilter
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Provider, Circuit, CircuitTermination, CircuitType
@@ -28,7 +28,6 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Site (slug)',
)
tag = TagFilter()
class Meta:
model = Provider
@@ -104,7 +103,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Site (slug)',
)
tag = TagFilter()
class Meta:
model = Circuit

View File

@@ -2,15 +2,14 @@ from __future__ import unicode_literals
from django import forms
from django.db.models import Count
from taggit.forms import TagField
from dcim.models import Site, Device, Interface, Rack
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
AnnotatedMultipleChoiceField, APISelect, add_blank_choice, BootstrapMixin, ChainedFieldsMixin,
ChainedModelChoiceField, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField,
APISelect, add_blank_choice, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField,
CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField,
)
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -23,11 +22,10 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderForm(BootstrapMixin, CustomFieldForm):
slug = SlugField()
comments = CommentField()
tags = TagField(required=False)
class Meta:
model = Provider
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags']
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
widgets = {
'noc_contact': SmallTextarea(attrs={'rows': 5}),
'admin_contact': SmallTextarea(attrs={'rows': 5}),
@@ -55,7 +53,7 @@ class ProviderCSVForm(forms.ModelForm):
}
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
asn = forms.IntegerField(required=False, label='ASN')
account = forms.CharField(max_length=30, required=False, label='Account number')
@@ -104,13 +102,12 @@ class CircuitTypeCSVForm(forms.ModelForm):
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
comments = CommentField()
tags = TagField(required=False)
class Meta:
model = Circuit
fields = [
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
'comments', 'tags',
'comments',
]
help_texts = {
'cid': "Unique circuit ID",
@@ -158,7 +155,7 @@ class CircuitCSVForm(forms.ModelForm):
]
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
@@ -172,6 +169,13 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
nullable_fields = ['tenant', 'commit_rate', 'description', 'comments']
def circuit_status_choices():
status_counts = {}
for status in Circuit.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count']
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in CIRCUIT_STATUS_CHOICES]
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit
q = forms.CharField(required=False, label='Search')
@@ -183,12 +187,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug'
)
status = AnnotatedMultipleChoiceField(
choices=CIRCUIT_STATUS_CHOICES,
annotate=Circuit.objects.all(),
annotate_field='status',
required=False
)
status = forms.MultipleChoiceField(choices=circuit_status_choices, required=False)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug',

View File

@@ -1,96 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:25
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
replaces = [('circuits', '0001_initial'), ('circuits', '0002_auto_20160622_1821'), ('circuits', '0003_provider_32bit_asn_support'), ('circuits', '0004_circuit_add_tenant'), ('circuits', '0005_circuit_add_upstream_speed'), ('circuits', '0006_terminations'), ('circuits', '0007_circuit_add_description'), ('circuits', '0008_circuittermination_interface_protect_on_delete'), ('circuits', '0009_unicode_literals'), ('circuits', '0010_circuit_status')]
dependencies = [
('dcim', '0001_initial'),
('dcim', '0022_color_names_to_rgb'),
('tenancy', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
('asn', dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN')),
('account', models.CharField(blank=True, max_length=30, verbose_name='Account number')),
('portal_url', models.URLField(blank=True, verbose_name='Portal')),
('noc_contact', models.TextField(blank=True, verbose_name='NOC contact')),
('admin_contact', models.TextField(blank=True, verbose_name='Admin contact')),
('comments', models.TextField(blank=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='CircuitType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Circuit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('cid', models.CharField(max_length=50, verbose_name='Circuit ID')),
('install_date', models.DateField(blank=True, null=True, verbose_name='Date installed')),
('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')),
('comments', models.TextField(blank=True)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.Provider')),
('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.CircuitType')),
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant')),
('description', models.CharField(blank=True, max_length=100)),
('status', models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1))
],
options={
'ordering': ['provider', 'cid'],
},
),
migrations.AlterUniqueTogether(
name='circuit',
unique_together=set([('provider', 'cid')]),
),
migrations.CreateModel(
name='CircuitTermination',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('term_side', models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination')),
('port_speed', models.PositiveIntegerField(verbose_name='Port speed (Kbps)')),
('upstream_speed', models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)')),
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID')),
('pp_info', models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)')),
('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.Circuit')),
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.Site')),
],
options={
'ordering': ['circuit', 'term_side'],
},
),
migrations.AlterUniqueTogether(
name='circuittermination',
unique_together=set([('circuit', 'term_side')]),
),
]

View File

@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-05-22 19:04
from __future__ import unicode_literals
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('taggit', '0002_auto_20150616_2121'),
('circuits', '0010_circuit_status'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='provider',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
]

View File

@@ -1,45 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-06-13 17:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0011_tags'),
]
operations = [
migrations.AddField(
model_name='circuittype',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='circuittype',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='circuit',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='circuit',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='provider',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='provider',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@@ -4,61 +4,30 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from taggit.managers import TaggableManager
from dcim.constants import STATUS_CLASSES
from dcim.fields import ASNField
from extras.models import CustomFieldModel, ObjectChange
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
@python_2_unicode_compatible
class Provider(ChangeLoggedModel, CustomFieldModel):
class Provider(CreatedUpdatedModel, CustomFieldModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider.
"""
name = models.CharField(
max_length=50,
unique=True
)
slug = models.SlugField(
unique=True
)
asn = ASNField(
blank=True,
null=True,
verbose_name='ASN'
)
account = models.CharField(
max_length=30,
blank=True,
verbose_name='Account number'
)
portal_url = models.URLField(
blank=True,
verbose_name='Portal'
)
noc_contact = models.TextField(
blank=True,
verbose_name='NOC contact'
)
admin_contact = models.TextField(
blank=True,
verbose_name='Admin contact'
)
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager()
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
asn = ASNField(blank=True, null=True, verbose_name='ASN')
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
portal_url = models.URLField(blank=True, verbose_name='Portal')
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
@@ -85,18 +54,13 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
@python_2_unicode_compatible
class CircuitType(ChangeLoggedModel):
class CircuitType(models.Model):
"""
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band".
"""
name = models.CharField(
max_length=50,
unique=True
)
slug = models.SlugField(
unique=True
)
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
csv_headers = ['name', 'slug']
@@ -117,60 +81,22 @@ class CircuitType(ChangeLoggedModel):
@python_2_unicode_compatible
class Circuit(ChangeLoggedModel, CustomFieldModel):
class Circuit(CreatedUpdatedModel, CustomFieldModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
interface, but this is not required. Circuit port speed and commit rate are measured in Kbps.
"""
cid = models.CharField(
max_length=50,
verbose_name='Circuit ID'
)
provider = models.ForeignKey(
to='circuits.Provider',
on_delete=models.PROTECT,
related_name='circuits'
)
type = models.ForeignKey(
to='CircuitType',
on_delete=models.PROTECT,
related_name='circuits'
)
status = models.PositiveSmallIntegerField(
choices=CIRCUIT_STATUS_CHOICES,
default=CIRCUIT_STATUS_ACTIVE
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='circuits',
blank=True,
null=True
)
install_date = models.DateField(
blank=True,
null=True,
verbose_name='Date installed'
)
commit_rate = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name='Commit rate (Kbps)')
description = models.CharField(
max_length=100,
blank=True
)
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager()
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
status = models.PositiveSmallIntegerField(choices=CIRCUIT_STATUS_CHOICES, default=CIRCUIT_STATUS_ACTIVE)
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
description = models.CharField(max_length=100, blank=True)
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
@@ -219,47 +145,19 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
@python_2_unicode_compatible
class CircuitTermination(models.Model):
circuit = models.ForeignKey(
to='circuits.Circuit',
on_delete=models.CASCADE,
related_name='terminations'
)
term_side = models.CharField(
max_length=1,
choices=TERM_SIDE_CHOICES,
verbose_name='Termination'
)
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.PROTECT,
related_name='circuit_terminations'
)
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT)
interface = models.OneToOneField(
to='dcim.Interface',
on_delete=models.PROTECT,
related_name='circuit_termination',
blank=True,
null=True
)
port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)'
'dcim.Interface', related_name='circuit_termination', blank=True, null=True, on_delete=models.PROTECT
)
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
upstream_speed = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name='Upstream speed (Kbps)',
blank=True, null=True, verbose_name='Upstream speed (Kbps)',
help_text='Upstream speed, if different from port speed'
)
xconnect_id = models.CharField(
max_length=50,
blank=True,
verbose_name='Cross-connect ID'
)
pp_info = models.CharField(
max_length=100,
blank=True,
verbose_name='Patch panel/port(s)'
)
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
class Meta:
ordering = ['circuit', 'term_side']
@@ -268,19 +166,6 @@ class CircuitTermination(models.Model):
def __str__(self):
return '{} (Side {})'.format(self.circuit, self.get_term_side_display())
def log_change(self, user, request_id, action):
"""
Reference the parent circuit when recording the change.
"""
ObjectChange(
user=user,
request_id=request_id,
changed_object=self,
related_object=self.circuit,
action=action,
object_data=serialize_object(self)
).save()
def get_peer_termination(self):
peer_side = 'Z' if self.term_side == 'A' else 'A'
try:

View File

@@ -9,9 +9,6 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_ACTIONS = """
<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}

View File

@@ -1,21 +1,26 @@
from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
from dcim.models import Site
from extras.constants import GRAPH_TYPE_PROVIDER
from extras.models import Graph
from utilities.testing import APITestCase
from users.models import Token
from utilities.tests import HttpStatusMixin
class ProviderTest(APITestCase):
class ProviderTest(HttpStatusMixin, APITestCase):
def setUp(self):
super(ProviderTest, self).setUp()
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
@@ -56,16 +61,6 @@ class ProviderTest(APITestCase):
self.assertEqual(response.data['count'], 3)
def test_list_providers_brief(self):
url = reverse('circuits-api:provider-list')
response = self.client.get('{}?brief=1'.format(url), **self.header)
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'slug', 'url']
)
def test_create_provider(self):
data = {
@@ -133,11 +128,13 @@ class ProviderTest(APITestCase):
self.assertEqual(Provider.objects.count(), 2)
class CircuitTypeTest(APITestCase):
class CircuitTypeTest(HttpStatusMixin, APITestCase):
def setUp(self):
super(CircuitTypeTest, self).setUp()
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
@@ -157,16 +154,6 @@ class CircuitTypeTest(APITestCase):
self.assertEqual(response.data['count'], 3)
def test_list_circuittypes_brief(self):
url = reverse('circuits-api:circuittype-list')
response = self.client.get('{}?brief=1'.format(url), **self.header)
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'slug', 'url']
)
def test_create_circuittype(self):
data = {
@@ -208,11 +195,13 @@ class CircuitTypeTest(APITestCase):
self.assertEqual(CircuitType.objects.count(), 2)
class CircuitTest(APITestCase):
class CircuitTest(HttpStatusMixin, APITestCase):
def setUp(self):
super(CircuitTest, self).setUp()
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
@@ -236,23 +225,12 @@ class CircuitTest(APITestCase):
self.assertEqual(response.data['count'], 3)
def test_list_circuits_brief(self):
url = reverse('circuits-api:circuit-list')
response = self.client.get('{}?brief=1'.format(url), **self.header)
self.assertEqual(
sorted(response.data['results'][0]),
['cid', 'id', 'url']
)
def test_create_circuit(self):
data = {
'cid': 'TEST0004',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
}
url = reverse('circuits-api:circuit-list')
@@ -272,19 +250,16 @@ class CircuitTest(APITestCase):
'cid': 'TEST0004',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
},
{
'cid': 'TEST0005',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
},
{
'cid': 'TEST0006',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
},
]
@@ -324,50 +299,29 @@ class CircuitTest(APITestCase):
self.assertEqual(Circuit.objects.count(), 2)
class CircuitTerminationTest(APITestCase):
class CircuitTerminationTest(HttpStatusMixin, APITestCase):
def setUp(self):
super(CircuitTerminationTest, self).setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_network_device=True
)
devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
)
device1 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=self.site1
)
device2 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Device 2', site=self.site2
)
self.interface1 = Interface.objects.create(device=device1, name='Test Interface 1')
self.interface2 = Interface.objects.create(device=device2, name='Test Interface 2')
self.interface3 = Interface.objects.create(device=device1, name='Test Interface 3')
self.interface4 = Interface.objects.create(device=device2, name='Test Interface 4')
self.interface5 = Interface.objects.create(device=device1, name='Test Interface 5')
self.interface6 = Interface.objects.create(device=device2, name='Test Interface 6')
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
provider = Provider.objects.create(name='Test Provider', slug='test-provider')
circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type')
self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=provider, type=circuittype)
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
self.circuittermination1 = CircuitTermination.objects.create(
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, interface=self.interface1, port_speed=1000000
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
)
self.circuittermination2 = CircuitTermination.objects.create(
circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, interface=self.interface2, port_speed=1000000
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
)
self.circuittermination3 = CircuitTermination.objects.create(
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, interface=self.interface3, port_speed=1000000
)
self.circuittermination4 = CircuitTermination.objects.create(
circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, interface=self.interface4, port_speed=1000000
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
)
def test_get_circuittermination(self):
@@ -382,15 +336,14 @@ class CircuitTerminationTest(APITestCase):
url = reverse('circuits-api:circuittermination-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 4)
self.assertEqual(response.data['count'], 3)
def test_create_circuittermination(self):
data = {
'circuit': self.circuit3.pk,
'term_side': TERM_SIDE_A,
'site': self.site1.pk,
'interface': self.interface5.pk,
'circuit': self.circuit1.pk,
'term_side': TERM_SIDE_Z,
'site': self.site2.pk,
'port_speed': 1000000,
}
@@ -398,37 +351,31 @@ class CircuitTerminationTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(CircuitTermination.objects.count(), 5)
self.assertEqual(CircuitTermination.objects.count(), 4)
circuittermination4 = CircuitTermination.objects.get(pk=response.data['id'])
self.assertEqual(circuittermination4.circuit_id, data['circuit'])
self.assertEqual(circuittermination4.term_side, data['term_side'])
self.assertEqual(circuittermination4.site_id, data['site'])
self.assertEqual(circuittermination4.interface_id, data['interface'])
self.assertEqual(circuittermination4.port_speed, data['port_speed'])
def test_update_circuittermination(self):
circuittermination5 = CircuitTermination.objects.create(
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, interface=self.interface5, port_speed=1000000
)
data = {
'circuit': self.circuit3.pk,
'circuit': self.circuit1.pk,
'term_side': TERM_SIDE_Z,
'site': self.site2.pk,
'interface': self.interface6.pk,
'port_speed': 1000000,
}
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': circuittermination5.pk})
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
response = self.client.put(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(CircuitTermination.objects.count(), 5)
self.assertEqual(CircuitTermination.objects.count(), 3)
circuittermination1 = CircuitTermination.objects.get(pk=response.data['id'])
self.assertEqual(circuittermination1.circuit_id, data['circuit'])
self.assertEqual(circuittermination1.term_side, data['term_side'])
self.assertEqual(circuittermination1.site_id, data['site'])
self.assertEqual(circuittermination1.interface_id, data['interface'])
self.assertEqual(circuittermination1.port_speed, data['port_speed'])
def test_delete_circuittermination(self):
@@ -437,4 +384,4 @@ class CircuitTerminationTest(APITestCase):
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(CircuitTermination.objects.count(), 3)
self.assertEqual(CircuitTermination.objects.count(), 2)

View File

@@ -2,9 +2,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from extras.views import ObjectChangeLogView
from . import views
from .models import Circuit, CircuitType, Provider
app_name = 'circuits'
urlpatterns = [
@@ -18,7 +16,6 @@ urlpatterns = [
url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
url(r'^providers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
# Circuit types
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
@@ -26,7 +23,6 @@ urlpatterns = [
url(r'^circuit-types/import/$', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
url(r'^circuit-types/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
# Circuits
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
@@ -37,7 +33,6 @@ urlpatterns = [
url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
url(r'^circuits/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
# Circuit terminations

View File

@@ -6,6 +6,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.generic import View
from extras.models import Graph, GRAPH_TYPE_PROVIDER
@@ -76,7 +77,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_provider'
queryset = Provider.objects.all()
cls = Provider
filter = filters.ProviderFilter
table = tables.ProviderTable
form = forms.ProviderBulkEditForm
@@ -85,7 +86,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider'
queryset = Provider.objects.all()
cls = Provider
filter = filters.ProviderFilter
table = tables.ProviderTable
default_return_url = 'circuits:provider_list'
@@ -105,7 +106,9 @@ class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittype'
model = CircuitType
model_form = forms.CircuitTypeForm
default_return_url = 'circuits:circuittype_list'
def get_return_url(self, request, obj):
return reverse('circuits:circuittype_list')
class CircuitTypeEditView(CircuitTypeCreateView):
@@ -121,6 +124,7 @@ class CircuitTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype'
cls = CircuitType
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable
default_return_url = 'circuits:circuittype_list'
@@ -192,6 +196,7 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_circuit'
cls = Circuit
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter
table = tables.CircuitTable
@@ -201,6 +206,7 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit'
cls = Circuit
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter
table = tables.CircuitTable

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,10 @@ from __future__ import unicode_literals
from collections import OrderedDict
from django.conf import settings
from django.http import HttpResponseForbidden
from django.db import transaction
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from drf_yasg import openapi
from drf_yasg.openapi import Parameter
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ViewSet
@@ -36,12 +34,11 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Device, ['face', 'status']),
(ConsolePort, ['connection_status']),
(Interface, ['form_factor', 'mode']),
(Interface, ['form_factor']),
(InterfaceConnection, ['connection_status']),
(InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']),
(Rack, ['type', 'width']),
(Site, ['status']),
)
@@ -52,6 +49,7 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
class RegionViewSet(ModelViewSet):
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
write_serializer_class = serializers.WritableRegionSerializer
filter_class = filters.RegionFilter
@@ -60,11 +58,12 @@ class RegionViewSet(ModelViewSet):
#
class SiteViewSet(CustomFieldModelViewSet):
queryset = Site.objects.select_related('region', 'tenant').prefetch_related('tags')
queryset = Site.objects.select_related('region', 'tenant')
serializer_class = serializers.SiteSerializer
write_serializer_class = serializers.WritableSiteSerializer
filter_class = filters.SiteFilter
@action(detail=True)
@detail_route()
def graphs(self, request, pk=None):
"""
A convenience method for rendering graphs for a particular site.
@@ -82,6 +81,7 @@ class SiteViewSet(CustomFieldModelViewSet):
class RackGroupViewSet(ModelViewSet):
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
write_serializer_class = serializers.WritableRackGroupSerializer
filter_class = filters.RackGroupFilter
@@ -100,11 +100,12 @@ class RackRoleViewSet(ModelViewSet):
#
class RackViewSet(CustomFieldModelViewSet):
queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('tags')
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
serializer_class = serializers.RackSerializer
write_serializer_class = serializers.WritableRackSerializer
filter_class = filters.RackFilter
@action(detail=True)
@detail_route()
def units(self, request, pk=None):
"""
List rack units (by rack)
@@ -132,6 +133,7 @@ class RackViewSet(CustomFieldModelViewSet):
class RackReservationViewSet(ModelViewSet):
queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
serializer_class = serializers.RackReservationSerializer
write_serializer_class = serializers.WritableRackReservationSerializer
filter_class = filters.RackReservationFilter
# Assign user from request
@@ -154,8 +156,9 @@ class ManufacturerViewSet(ModelViewSet):
#
class DeviceTypeViewSet(CustomFieldModelViewSet):
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags')
queryset = DeviceType.objects.select_related('manufacturer')
serializer_class = serializers.DeviceTypeSerializer
write_serializer_class = serializers.WritableDeviceTypeSerializer
filter_class = filters.DeviceTypeFilter
@@ -166,36 +169,42 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
class ConsolePortTemplateViewSet(ModelViewSet):
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.ConsolePortTemplateSerializer
write_serializer_class = serializers.WritableConsolePortTemplateSerializer
filter_class = filters.ConsolePortTemplateFilter
class ConsoleServerPortTemplateViewSet(ModelViewSet):
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.ConsoleServerPortTemplateSerializer
write_serializer_class = serializers.WritableConsoleServerPortTemplateSerializer
filter_class = filters.ConsoleServerPortTemplateFilter
class PowerPortTemplateViewSet(ModelViewSet):
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.PowerPortTemplateSerializer
write_serializer_class = serializers.WritablePowerPortTemplateSerializer
filter_class = filters.PowerPortTemplateFilter
class PowerOutletTemplateViewSet(ModelViewSet):
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.PowerOutletTemplateSerializer
write_serializer_class = serializers.WritablePowerOutletTemplateSerializer
filter_class = filters.PowerOutletTemplateFilter
class InterfaceTemplateViewSet(ModelViewSet):
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.InterfaceTemplateSerializer
write_serializer_class = serializers.WritableInterfaceTemplateSerializer
filter_class = filters.InterfaceTemplateFilter
class DeviceBayTemplateViewSet(ModelViewSet):
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer
write_serializer_class = serializers.WritableDeviceBayTemplateSerializer
filter_class = filters.DeviceBayTemplateFilter
@@ -216,6 +225,7 @@ class DeviceRoleViewSet(ModelViewSet):
class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
write_serializer_class = serializers.WritablePlatformSerializer
filter_class = filters.PlatformFilter
@@ -228,24 +238,13 @@ class DeviceViewSet(CustomFieldModelViewSet):
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
'virtual_chassis__master',
).prefetch_related(
'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
)
serializer_class = serializers.DeviceSerializer
write_serializer_class = serializers.WritableDeviceSerializer
filter_class = filters.DeviceFilter
def get_serializer_class(self):
"""
Include rendered config context when retrieving a single Device.
"""
if self.action == 'retrieve':
return serializers.DeviceWithConfigContextSerializer
request = self.get_serializer_context()['request']
if request.query_params.get('brief', False):
return serializers.NestedDeviceSerializer
return serializers.DeviceSerializer
@action(detail=True, url_path='napalm')
@detail_route(url_path='napalm')
def napalm(self, request, pk):
"""
Execute a NAPALM method on a Device
@@ -263,9 +262,9 @@ class DeviceViewSet(CustomFieldModelViewSet):
# Check that NAPALM is installed
try:
import napalm
from napalm.base.exceptions import ModuleImportError
except ImportError:
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
# Validate the configured driver
try:
@@ -279,41 +278,33 @@ class DeviceViewSet(CustomFieldModelViewSet):
if not request.user.has_perm('dcim.napalm_read'):
return HttpResponseForbidden()
# Connect to the device
# Validate requested NAPALM methods
napalm_methods = request.GET.getlist('method')
for method in napalm_methods:
if not hasattr(driver, method):
return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method))
elif not method.startswith('get_'):
return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method))
# Connect to the device and execute the requested methods
# TODO: Improve error handling
response = OrderedDict([(m, None) for m in napalm_methods])
ip_address = str(device.primary_ip.address.ip)
optional_args = settings.NAPALM_ARGS.copy()
if device.platform.napalm_args is not None:
optional_args.update(device.platform.napalm_args)
d = driver(
hostname=ip_address,
username=settings.NAPALM_USERNAME,
password=settings.NAPALM_PASSWORD,
timeout=settings.NAPALM_TIMEOUT,
optional_args=optional_args
optional_args=settings.NAPALM_ARGS
)
try:
d.open()
for method in napalm_methods:
response[method] = getattr(d, method)()
except Exception as e:
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
# Validate and execute each specified NAPALM method
for method in napalm_methods:
if not hasattr(driver, method):
response[method] = {'error': 'Unknown NAPALM method'}
continue
if not method.startswith('get_'):
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
continue
try:
response[method] = getattr(d, method)()
except NotImplementedError:
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
except Exception as e:
response[method] = {'error': 'Method {} failed: {}'.format(method, e)}
d.close()
return Response(response)
@@ -322,35 +313,40 @@ class DeviceViewSet(CustomFieldModelViewSet):
#
class ConsolePortViewSet(ModelViewSet):
queryset = ConsolePort.objects.select_related('device', 'cs_port__device').prefetch_related('tags')
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
serializer_class = serializers.ConsolePortSerializer
write_serializer_class = serializers.WritableConsolePortSerializer
filter_class = filters.ConsolePortFilter
class ConsoleServerPortViewSet(ModelViewSet):
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device').prefetch_related('tags')
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
serializer_class = serializers.ConsoleServerPortSerializer
write_serializer_class = serializers.WritableConsoleServerPortSerializer
filter_class = filters.ConsoleServerPortFilter
class PowerPortViewSet(ModelViewSet):
queryset = PowerPort.objects.select_related('device', 'power_outlet__device').prefetch_related('tags')
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
serializer_class = serializers.PowerPortSerializer
write_serializer_class = serializers.WritablePowerPortSerializer
filter_class = filters.PowerPortFilter
class PowerOutletViewSet(ModelViewSet):
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device').prefetch_related('tags')
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
serializer_class = serializers.PowerOutletSerializer
write_serializer_class = serializers.WritablePowerOutletSerializer
filter_class = filters.PowerOutletFilter
class InterfaceViewSet(ModelViewSet):
queryset = Interface.objects.select_related('device').prefetch_related('tags')
queryset = Interface.objects.select_related('device')
serializer_class = serializers.InterfaceSerializer
write_serializer_class = serializers.WritableInterfaceSerializer
filter_class = filters.InterfaceFilter
@action(detail=True)
@detail_route()
def graphs(self, request, pk=None):
"""
A convenience method for rendering graphs for a particular interface.
@@ -362,14 +358,16 @@ class InterfaceViewSet(ModelViewSet):
class DeviceBayViewSet(ModelViewSet):
queryset = DeviceBay.objects.select_related('installed_device').prefetch_related('tags')
queryset = DeviceBay.objects.select_related('installed_device')
serializer_class = serializers.DeviceBaySerializer
write_serializer_class = serializers.WritableDeviceBaySerializer
filter_class = filters.DeviceBayFilter
class InventoryItemViewSet(ModelViewSet):
queryset = InventoryItem.objects.select_related('device', 'manufacturer').prefetch_related('tags')
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
serializer_class = serializers.InventoryItemSerializer
write_serializer_class = serializers.WritableInventoryItemSerializer
filter_class = filters.InventoryItemFilter
@@ -392,6 +390,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
class InterfaceConnectionViewSet(ModelViewSet):
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
serializer_class = serializers.InterfaceConnectionSerializer
write_serializer_class = serializers.WritableInterfaceConnectionSerializer
filter_class = filters.InterfaceConnectionFilter
@@ -400,8 +399,9 @@ class InterfaceConnectionViewSet(ModelViewSet):
#
class VirtualChassisViewSet(ModelViewSet):
queryset = VirtualChassis.objects.prefetch_related('tags')
queryset = VirtualChassis.objects.all()
serializer_class = serializers.VirtualChassisSerializer
write_serializer_class = serializers.WritableVirtualChassisSerializer
#
@@ -414,32 +414,20 @@ class ConnectedDeviceViewSet(ViewSet):
interface. This is useful in a situation where a device boots with no configuration, but can detect its neighbors
via a protocol such as LLDP. Two query parameters must be included in the request:
* `peer_device`: The name of the peer device
* `peer_interface`: The name of the peer interface
* `peer-device`: The name of the peer device
* `peer-interface`: The name of the peer interface
"""
permission_classes = [IsAuthenticatedOrLoginNotRequired]
_device_param = Parameter('peer_device', 'query',
description='The name of the peer device', required=True, type=openapi.TYPE_STRING)
_interface_param = Parameter('peer_interface', 'query',
description='The name of the peer interface', required=True, type=openapi.TYPE_STRING)
def get_view_name(self):
return "Connected Device Locator"
@swagger_auto_schema(
manual_parameters=[_device_param, _interface_param], responses={'200': serializers.DeviceSerializer})
def list(self, request):
peer_device_name = request.query_params.get(self._device_param.name)
if not peer_device_name:
# TODO: remove this after 2.4 as the switch to using underscores is a breaking change
peer_device_name = request.query_params.get('peer-device')
peer_interface_name = request.query_params.get(self._interface_param.name)
if not peer_interface_name:
# TODO: remove this after 2.4 as the switch to using underscores is a breaking change
peer_interface_name = request.query_params.get('peer-interface')
peer_device_name = request.query_params.get('peer-device')
peer_interface_name = request.query_params.get('peer-interface')
if not peer_device_name or not peer_interface_name:
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')
# Determine local interface from peer interface's connection
peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)

View File

@@ -8,5 +8,4 @@ class DCIMConfig(AppConfig):
verbose_name = "DCIM"
def ready(self):
import dcim.signals

View File

@@ -93,11 +93,6 @@ IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_FLEXSTACK = 5100
IFACE_FF_FLEXSTACK_PLUS = 5150
IFACE_FF_JUNIPER_VCP = 5200
IFACE_FF_SUMMITSTACK = 5300
IFACE_FF_SUMMITSTACK128 = 5310
IFACE_FF_SUMMITSTACK256 = 5320
IFACE_FF_SUMMITSTACK512 = 5330
# Other
IFACE_FF_OTHER = 32767
@@ -173,10 +168,6 @@ IFACE_FF_CHOICES = [
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
[IFACE_FF_SUMMITSTACK, 'Extreme SummitStack'],
[IFACE_FF_SUMMITSTACK128, 'Extreme SummitStack-128'],
[IFACE_FF_SUMMITSTACK256, 'Extreme SummitStack-256'],
[IFACE_FF_SUMMITSTACK512, 'Extreme SummitStack-512'],
]
],
[

View File

@@ -1,11 +1,13 @@
from __future__ import unicode_literals
from netaddr import AddrFormatError, EUI, mac_unix_expanded
from netaddr import EUI, mac_unix_expanded
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from .formfields import MACAddressFormField
class ASNField(models.BigIntegerField):
description = "32-bit ASN field"
@@ -33,7 +35,7 @@ class MACAddressField(models.Field):
return value
try:
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
except AddrFormatError as e:
except ValueError as e:
raise ValidationError(e)
def db_type(self, connection):
@@ -43,3 +45,11 @@ class MACAddressField(models.Field):
if not value:
return None
return str(self.to_python(value))
def form_class(self):
return MACAddressFormField
def formfield(self, **kwargs):
defaults = {'form_class': self.form_class()}
defaults.update(kwargs)
return super(MACAddressField, self).formfield(**defaults)

View File

@@ -2,14 +2,13 @@ from __future__ import unicode_literals
import django_filters
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from netaddr import EUI
from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter
from utilities.filters import NullableCharFieldFilter, NumericInFilter
from virtualization.models import Cluster
from .constants import (
DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES,
@@ -83,7 +82,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Tenant (slug)',
)
tag = TagFilter()
class Meta:
model = Site
@@ -111,10 +109,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class RackGroupFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@@ -130,15 +124,6 @@ class RackGroupFilter(django_filters.FilterSet):
model = RackGroup
fields = ['site_id', 'name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class RackRoleFilter(django_filters.FilterSet):
@@ -194,7 +179,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Role (slug)',
)
tag = TagFilter()
class Meta:
model = Rack
@@ -302,7 +286,6 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Manufacturer (slug)',
)
tag = TagFilter()
class Meta:
model = DeviceType
@@ -451,16 +434,6 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
)
name = NullableCharFieldFilter()
asset_tag = NullableCharFieldFilter()
region_id = django_filters.NumberFilter(
method='filter_region',
name='pk',
label='Region (ID)',
)
region = django_filters.CharFilter(
method='filter_region',
name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@@ -524,7 +497,6 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=VirtualChassis.objects.all(),
label='Virtual chassis (ID)',
)
tag = TagFilter()
class Meta:
model = Device
@@ -537,20 +509,10 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(name__icontains=value) |
Q(serial__icontains=value.strip()) |
Q(inventory_items__serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
Q(asset_tag=value.strip()) |
Q(comments__icontains=value)
).distinct()
def filter_region(self, queryset, name, value):
try:
region = Region.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
return queryset.filter(
Q(site__region=region) |
Q(site__region__in=region.get_descendants())
)
def _mac_address(self, queryset, name, value):
value = value.strip()
if not value:
@@ -584,7 +546,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='name',
label='Device (name)',
)
tag = TagFilter()
class ConsolePortFilter(DeviceComponentFilterSet):
@@ -643,15 +604,6 @@ class InterfaceFilter(django_filters.FilterSet):
method='_mac_address',
label='MAC address',
)
tag = TagFilter()
vlan_id = django_filters.CharFilter(
method='filter_vlan_id',
label='Assigned VLAN'
)
vlan = django_filters.CharFilter(
method='filter_vlan',
label='Assigned VID'
)
class Meta:
model = Interface
@@ -666,24 +618,6 @@ class InterfaceFilter(django_filters.FilterSet):
except Device.DoesNotExist:
return queryset.none()
def filter_vlan_id(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
return queryset.filter(
Q(untagged_vlan_id=value) |
Q(tagged_vlans=value)
)
def filter_vlan(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
return queryset.filter(
Q(untagged_vlan_id__vid=value) |
Q(tagged_vlans__vid=value)
)
def filter_type(self, queryset, name, value):
value = value.strip().lower()
return {
@@ -716,15 +650,6 @@ class InventoryItemFilter(DeviceComponentFilterSet):
method='search',
label='Search',
)
device_id = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)',
@@ -759,47 +684,11 @@ class InventoryItemFilter(DeviceComponentFilterSet):
class VirtualChassisFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='master__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='master__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site name (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='master__tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='master__tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
tag = TagFilter()
class Meta:
model = VirtualChassis
fields = ['domain']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(master__name__icontains=value) |
Q(domain__icontains=value)
)
return queryset.filter(qs_filter)
class ConsoleConnectionFilter(django_filters.FilterSet):
site = django_filters.CharFilter(

View File

@@ -70,8 +70,6 @@
"model": "dcim.devicetype",
"pk": 1,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"manufacturer": 1,
"model": "MX960",
"slug": "mx960",
@@ -86,8 +84,6 @@
"model": "dcim.devicetype",
"pk": 2,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"manufacturer": 1,
"model": "EX9214",
"slug": "ex9214",
@@ -102,8 +98,6 @@
"model": "dcim.devicetype",
"pk": 3,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"manufacturer": 1,
"model": "QFX5100-24Q",
"slug": "qfx5100-24q",
@@ -118,8 +112,6 @@
"model": "dcim.devicetype",
"pk": 4,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"manufacturer": 1,
"model": "QFX5100-48S",
"slug": "qfx5100-48s",
@@ -134,8 +126,6 @@
"model": "dcim.devicetype",
"pk": 5,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"manufacturer": 2,
"model": "CM4148",
"slug": "cm4148",
@@ -150,8 +140,6 @@
"model": "dcim.devicetype",
"pk": 6,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"manufacturer": 3,
"model": "CWG-24VYM415C9",
"slug": "cwg-24vym415c9",

27
netbox/dcim/formfields.py Normal file
View File

@@ -0,0 +1,27 @@
from __future__ import unicode_literals
from django import forms
from django.core.exceptions import ValidationError
from netaddr import EUI, AddrFormatError
#
# Form fields
#
class MACAddressFormField(forms.Field):
default_error_messages = {
'invalid': "Enter a valid MAC address.",
}
def to_python(self, value):
if not value:
return None
if isinstance(value, EUI):
return value
try:
return EUI(value, version=48)
except AddrFormatError:
raise ValidationError("Please specify a valid MAC address.")

File diff suppressed because it is too large Load Diff

View File

@@ -1,261 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:06
from __future__ import unicode_literals
import dcim.fields
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import utilities.fields
class Migration(migrations.Migration):
replaces = [('dcim', '0002_auto_20160622_1821'), ('dcim', '0003_auto_20160628_1721'), ('dcim', '0004_auto_20160701_2049'), ('dcim', '0005_auto_20160706_1722'), ('dcim', '0006_add_device_primary_ip4_ip6'), ('dcim', '0007_device_copy_primary_ip'), ('dcim', '0008_device_remove_primary_ip'), ('dcim', '0009_site_32bit_asn_support'), ('dcim', '0010_devicebay_installed_device_set_null'), ('dcim', '0011_devicetype_part_number'), ('dcim', '0012_site_rack_device_add_tenant'), ('dcim', '0013_add_interface_form_factors'), ('dcim', '0014_rack_add_type_width'), ('dcim', '0015_rack_add_u_height_validator'), ('dcim', '0016_module_add_manufacturer'), ('dcim', '0017_rack_add_role'), ('dcim', '0018_device_add_asset_tag'), ('dcim', '0019_new_iface_form_factors'), ('dcim', '0020_rack_desc_units'), ('dcim', '0021_add_ff_flexstack'), ('dcim', '0022_color_names_to_rgb')]
dependencies = [
('dcim', '0001_initial'),
('ipam', '0001_initial'),
('tenancy', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='device',
name='rack',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='device_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_port_templates', to='dcim.DeviceType'),
),
migrations.AddField(
model_name='consoleserverport',
name='device',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_ports', to='dcim.Device'),
),
migrations.AddField(
model_name='consoleporttemplate',
name='device_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_port_templates', to='dcim.DeviceType'),
),
migrations.AddField(
model_name='consoleport',
name='cs_port',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name=b'Console server port'),
),
migrations.AddField(
model_name='consoleport',
name='device',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_ports', to='dcim.Device'),
),
migrations.AlterUniqueTogether(
name='rackgroup',
unique_together=set([('site', 'name'), ('site', 'slug')]),
),
migrations.AlterUniqueTogether(
name='rack',
unique_together=set([('site', 'facility_id'), ('site', 'name')]),
),
migrations.AlterUniqueTogether(
name='powerporttemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='powerport',
unique_together=set([('device', 'name')]),
),
migrations.AlterUniqueTogether(
name='poweroutlettemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='poweroutlet',
unique_together=set([('device', 'name')]),
),
migrations.AlterUniqueTogether(
name='module',
unique_together=set([('device', 'parent', 'name')]),
),
migrations.AlterUniqueTogether(
name='interfacetemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AddField(
model_name='interface',
name='mac_address',
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'),
),
migrations.AlterUniqueTogether(
name='interface',
unique_together=set([('device', 'name')]),
),
migrations.AddField(
model_name='devicetype',
name='subdevice_role',
field=models.NullBooleanField(choices=[(None, b'None'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'),
),
migrations.AlterUniqueTogether(
name='devicetype',
unique_together=set([('manufacturer', 'slug'), ('manufacturer', 'model')]),
),
migrations.AlterUniqueTogether(
name='device',
unique_together=set([('rack', 'position', 'face')]),
),
migrations.AlterUniqueTogether(
name='consoleserverporttemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='consoleserverport',
unique_together=set([('device', 'name')]),
),
migrations.AlterUniqueTogether(
name='consoleporttemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='consoleport',
unique_together=set([('device', 'name')]),
),
migrations.CreateModel(
name='DeviceBay',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name=b'Name')),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bays', to='dcim.Device')),
('installed_device', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_bay', to='dcim.Device')),
],
options={
'ordering': ['device', 'name'],
},
),
migrations.CreateModel(
name='DeviceBayTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30)),
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bay_templates', to='dcim.DeviceType')),
],
options={
'ordering': ['device_type', 'name'],
},
),
migrations.AlterUniqueTogether(
name='devicebaytemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='devicebay',
unique_together=set([('device', 'name')]),
),
migrations.AddField(
model_name='device',
name='primary_ip4',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name=b'Primary IPv4'),
),
migrations.AddField(
model_name='device',
name='primary_ip6',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name=b'Primary IPv6'),
),
migrations.AlterField(
model_name='site',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
),
migrations.AlterField(
model_name='devicebay',
name='installed_device',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_bay', to='dcim.Device'),
),
migrations.AddField(
model_name='devicetype',
name='part_number',
field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50),
),
migrations.AddField(
model_name='device',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='rack',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='site',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='rack',
name='type',
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'),
),
migrations.AddField(
model_name='rack',
name='width',
field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'),
),
migrations.AlterField(
model_name='rack',
name='u_height',
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'),
),
migrations.AddField(
model_name='module',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'),
),
migrations.CreateModel(
name='RackRole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
('color', utilities.fields.ColorField(max_length=6)),
],
options={
'ordering': ['name'],
},
),
migrations.AddField(
model_name='rack',
name='role',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'),
),
migrations.AddField(
model_name='device',
name='asset_tag',
field=utilities.fields.NullableCharField(blank=True, help_text=b'A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name=b'Asset tag'),
),
migrations.AddField(
model_name='rack',
name='desc_units',
field=models.BooleanField(default=False, help_text=b'Units are numbered top-to-bottom', verbose_name=b'Descending units'),
),
migrations.AlterField(
model_name='device',
name='position',
field=models.PositiveSmallIntegerField(blank=True, help_text=b'The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.AlterField(
model_name='devicerole',
name='color',
field=utilities.fields.ColorField(max_length=6),
),
]

View File

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

View File

@@ -1,146 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-31 02:17
from __future__ import unicode_literals
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import timezone_field.fields
import utilities.fields
class Migration(migrations.Migration):
replaces = [('dcim', '0044_virtualization'), ('dcim', '0045_devicerole_vm_role'), ('dcim', '0046_rack_lengthen_facility_id'), ('dcim', '0047_more_100ge_form_factors'), ('dcim', '0048_rack_serial'), ('dcim', '0049_rackreservation_change_user'), ('dcim', '0050_interface_vlan_tagging'), ('dcim', '0051_rackreservation_tenant'), ('dcim', '0052_virtual_chassis'), ('dcim', '0053_platform_manufacturer'), ('dcim', '0054_site_status_timezone_description'), ('dcim', '0055_virtualchassis_ordering')]
dependencies = [
('dcim', '0043_device_component_name_lengths'),
('ipam', '0020_ipaddress_add_role_carp'),
('virtualization', '0001_virtualization'),
('tenancy', '0003_unicode_literals'),
]
operations = [
migrations.AddField(
model_name='device',
name='cluster',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
),
migrations.AddField(
model_name='interface',
name='virtual_machine',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'),
),
migrations.AlterField(
model_name='interface',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
),
migrations.AddField(
model_name='devicerole',
name='vm_role',
field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'),
),
migrations.AlterField(
model_name='rack',
name='facility_id',
field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AddField(
model_name='rack',
name='serial',
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
),
migrations.AlterField(
model_name='rackreservation',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='interface',
name='mode',
field=models.PositiveSmallIntegerField(blank=True, choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], null=True),
),
migrations.AddField(
model_name='interface',
name='tagged_vlans',
field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'),
),
migrations.AddField(
model_name='interface',
name='untagged_vlan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
),
migrations.AddField(
model_name='rackreservation',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'),
),
migrations.CreateModel(
name='VirtualChassis',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('domain', models.CharField(blank=True, max_length=30)),
('master', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')),
],
options={
'verbose_name_plural': 'virtual chassis',
'ordering': ['master'],
},
),
migrations.AddField(
model_name='device',
name='virtual_chassis',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'),
),
migrations.AddField(
model_name='device',
name='vc_position',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
),
migrations.AddField(
model_name='device',
name='vc_priority',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
),
migrations.AlterUniqueTogether(
name='device',
unique_together=set([('rack', 'position', 'face'), ('virtual_chassis', 'vc_position')]),
),
migrations.AddField(
model_name='platform',
name='manufacturer',
field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='platforms', to='dcim.Manufacturer'),
),
migrations.AlterField(
model_name='platform',
name='napalm_driver',
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'),
),
migrations.AddField(
model_name='site',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='site',
name='status',
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1),
),
migrations.AddField(
model_name='site',
name='time_zone',
field=timezone_field.fields.TimeZoneField(blank=True),
),
]

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-02-21 14:41
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0054_site_status_timezone_description'),
]
operations = [
migrations.AlterModelOptions(
name='virtualchassis',
options={'ordering': ['master'], 'verbose_name_plural': 'virtual chassis'},
),
migrations.AlterField(
model_name='virtualchassis',
name='master',
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device'),
),
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 2.0.3 on 2018-03-30 14:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0055_virtualchassis_ordering'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='untagged_vlan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
),
migrations.AlterField(
model_name='platform',
name='manufacturer',
field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='platforms', to='dcim.Manufacturer'),
),
]

View File

@@ -1,77 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-05-22 19:04
from __future__ import unicode_literals
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('taggit', '0002_auto_20150616_2121'),
('dcim', '0056_django2'),
]
operations = [
migrations.AddField(
model_name='device',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='devicetype',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='rack',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='site',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='consoleport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='consoleserverport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='devicebay',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='interface',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='inventoryitem',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='poweroutlet',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='powerport',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='virtualchassis',
name='tags',
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
),
]

View File

@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-05-22 19:27
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0057_tags'),
]
operations = [
migrations.AlterModelOptions(
name='rack',
options={'ordering': ['site', 'group', 'name']},
),
migrations.AlterUniqueTogether(
name='rack',
unique_together=set([('group', 'name'), ('group', 'facility_id')]),
),
]

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-06-21 18:45
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0058_relax_rack_naming_constraints'),
]
operations = [
migrations.AddField(
model_name='site',
name='latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True),
),
migrations.AddField(
model_name='site',
name='longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
]

View File

@@ -1,135 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-06-13 17:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0059_site_latitude_longitude'),
]
operations = [
migrations.AddField(
model_name='devicerole',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='devicerole',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='devicetype',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='devicetype',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='manufacturer',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='manufacturer',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='platform',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='platform',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rackgroup',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='rackgroup',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rackreservation',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rackrole',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='rackrole',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='region',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='region',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='virtualchassis',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='virtualchassis',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='device',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='device',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='rack',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='rack',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='rackreservation',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='site',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='site',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 2.0.6 on 2018-06-29 15:02
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0060_change_logging'),
]
operations = [
migrations.AddField(
model_name='platform',
name='napalm_args',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)', null=True, verbose_name='NAPALM arguments'),
),
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 2.0.8 on 2018-08-22 14:23
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0061_platform_napalm_args'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='mtu',
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 2.0.8 on 2018-09-16 02:01
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0062_interface_mtu'),
]
operations = [
migrations.AddField(
model_name='device',
name='local_context_data',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -43,13 +43,13 @@ class InterfaceQuerySet(QuerySet):
}[method]
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(\d{{1,9}})$') AS integer)"
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})\/') AS integer)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/)(\d{{1,9}})') AS integer), 0)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{2}}(\d{{1,9}})') AS integer), 0)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{3}}(\d{{1,9}})') AS integer), 0)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.(\d{{1,9}})$') AS integer), 0)"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
fields = {
'_type': RawSQL(TYPE_RE.format(sql_col), []),

View File

@@ -12,7 +12,7 @@ def assign_virtualchassis_master(instance, created, **kwargs):
When a VirtualChassis is created, automatically assign its master device to the VC.
"""
if created:
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=None)
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=1)
@receiver(pre_delete, sender=VirtualChassis)

View File

@@ -4,12 +4,12 @@ import django_tables2 as tables
from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
from utilities.tables import BaseTable, ToggleColumn
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, InventoryItem,
Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
VirtualChassis,
)
REGION_LINK = """
@@ -41,32 +41,18 @@ DEVICE_LINK = """
"""
REGION_ACTIONS = """
<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_region %}
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
RACKGROUP_ACTIONS = """
<a href="{% url 'dcim:rackgroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
<i class="fa fa-eye"></i>
</a>
{% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning" title="Edit">
<i class="glyphicon glyphicon-pencil"></i>
</a>
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
RACKROLE_ACTIONS = """
<a href="{% url 'dcim:rackrole_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_rackrole %}
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
@@ -85,29 +71,20 @@ RACK_DEVICE_COUNT = """
"""
RACKRESERVATION_ACTIONS = """
<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_rackreservation %}
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
MANUFACTURER_ACTIONS = """
<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_manufacturer %}
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
DEVICEROLE_ACTIONS = """
{% if perms.dcim.change_devicerole %}
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
DEVICEROLE_ACTIONS = """
<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_devicerole %}
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
MANUFACTURER_ACTIONS = """
{% if perms.dcim.change_manufacturer %}
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -128,9 +105,6 @@ PLATFORM_VM_COUNT = """
"""
PLATFORM_ACTIONS = """
<a href="{% url 'dcim:platform_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_platform %}
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
@@ -154,19 +128,12 @@ SUBDEVICE_ROLE_TEMPLATE = """
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}&mdash;{% endif %}
"""
DEVICETYPE_INSTANCES_TEMPLATE = """
<a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
"""
UTILIZATION_GRAPH = """
{% load helpers %}
{% utilization_graph value %}
"""
VIRTUALCHASSIS_ACTIONS = """
<a href="{% url 'dcim:virtualchassis_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_virtualchassis %}
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
@@ -199,7 +166,7 @@ class RegionTable(BaseTable):
class SiteTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
name = tables.LinkColumn()
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
tenant = tables.TemplateColumn(template_code=COL_TENANT)
@@ -215,21 +182,12 @@ class SiteTable(BaseTable):
class RackGroupTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
site = tables.LinkColumn(
viewname='dcim:site',
args=[Accessor('site.slug')],
verbose_name='Site'
)
rack_count = tables.Column(
verbose_name='Racks'
)
slug = tables.Column()
actions = tables.TemplateColumn(
template_code=RACKGROUP_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
name = tables.LinkColumn(verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
rack_count = tables.Column(verbose_name='Racks')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = RackGroup
@@ -250,7 +208,7 @@ class RackRoleTable(BaseTable):
verbose_name='')
class Meta(BaseTable.Meta):
model = RackRole
model = RackGroup
fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions')
@@ -260,7 +218,7 @@ class RackRoleTable(BaseTable):
class RackTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
name = tables.LinkColumn()
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
@@ -341,23 +299,13 @@ class ManufacturerTable(BaseTable):
class DeviceTypeTable(BaseTable):
pk = ToggleColumn()
model = tables.LinkColumn(
viewname='dcim:devicetype',
args=[Accessor('pk')],
verbose_name='Device Type'
)
is_full_depth = BooleanColumn(verbose_name='Full Depth')
is_console_server = BooleanColumn(verbose_name='CS')
is_pdu = BooleanColumn(verbose_name='PDU')
is_network_device = BooleanColumn(verbose_name='Net')
subdevice_role = tables.TemplateColumn(
template_code=SUBDEVICE_ROLE_TEMPLATE,
verbose_name='Subdevice Role'
)
instance_count = tables.TemplateColumn(
template_code=DEVICETYPE_INSTANCES_TEMPLATE,
verbose_name='Instances'
)
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
is_console_server = tables.BooleanColumn(verbose_name='CS')
is_pdu = tables.BooleanColumn(verbose_name='PDU')
is_network_device = tables.BooleanColumn(verbose_name='Net')
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
instance_count = tables.Column(verbose_name='Instances')
class Meta(BaseTable.Meta):
model = DeviceType
@@ -432,6 +380,7 @@ class DeviceBayTemplateTable(BaseTable):
class DeviceRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.TemplateColumn(
template_code=DEVICEROLE_DEVICE_COUNT,
accessor=Accessor('devices.count'),
@@ -492,10 +441,7 @@ class PlatformTable(BaseTable):
class DeviceTable(BaseTable):
pk = ToggleColumn()
name = tables.TemplateColumn(
order_by=('_nat1', '_nat2', '_nat3'),
template_code=DEVICE_LINK
)
name = tables.TemplateColumn(template_code=DEVICE_LINK)
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
@@ -614,15 +560,13 @@ class PowerConnectionTable(BaseTable):
class InterfaceConnectionTable(BaseTable):
device_a = tables.LinkColumn('dcim:device', accessor=Accessor('interface_a.device'),
args=[Accessor('interface_a.device.pk')], verbose_name='Device A')
interface_a = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_a'),
args=[Accessor('interface_a.pk')], verbose_name='Interface A')
interface_a = tables.Column(verbose_name='Interface A')
device_b = tables.LinkColumn('dcim:device', accessor=Accessor('interface_b.device'),
args=[Accessor('interface_b.device.pk')], verbose_name='Device B')
interface_b = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_b'),
args=[Accessor('interface_b.pk')], verbose_name='Interface B')
interface_b = tables.Column(verbose_name='Interface B')
class Meta(BaseTable.Meta):
model = InterfaceConnection
model = Interface
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,11 @@ from __future__ import unicode_literals
from django.conf.urls import url
from extras.views import ObjectChangeLogView, ImageAttachmentEditView
from extras.views import ImageAttachmentEditView
from ipam.views import ServiceCreateView
from secrets.views import secret_add
from . import views
from .models import (
Device, DeviceRole, DeviceType, Interface, Manufacturer, Platform, Rack, RackGroup, RackReservation, RackRole,
Region, Site, VirtualChassis,
)
from .models import Device, Rack, Site
app_name = 'dcim'
urlpatterns = [
@@ -20,7 +17,6 @@ urlpatterns = [
url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'),
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
url(r'^regions/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
# Sites
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
@@ -30,7 +26,6 @@ urlpatterns = [
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
url(r'^sites/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
# Rack groups
@@ -39,7 +34,6 @@ urlpatterns = [
url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
url(r'^rack-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
# Rack roles
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
@@ -47,7 +41,6 @@ urlpatterns = [
url(r'^rack-roles/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
url(r'^rack-roles/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
# Rack reservations
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
@@ -55,7 +48,6 @@ urlpatterns = [
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
url(r'^rack-reservations/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
# Racks
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
@@ -67,7 +59,6 @@ urlpatterns = [
url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
url(r'^racks/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
@@ -77,7 +68,6 @@ urlpatterns = [
url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
url(r'^manufacturers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
# Device types
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
@@ -88,7 +78,6 @@ urlpatterns = [
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
url(r'^device-types/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
# Console port templates
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
@@ -121,7 +110,6 @@ urlpatterns = [
url(r'^device-roles/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
url(r'^device-roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
# Platforms
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
@@ -129,7 +117,6 @@ urlpatterns = [
url(r'^platforms/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'),
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
url(r'^platforms/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
# Devices
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
@@ -141,8 +128,6 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
url(r'^devices/(?P<pk>\d+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
url(r'^devices/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
@@ -199,11 +184,8 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'),
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'),
url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'),
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
url(r'^interfaces/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
# Device bays
@@ -238,7 +220,6 @@ urlpatterns = [
url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
url(r'^virtual-chassis/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),

View File

@@ -7,21 +7,19 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db import transaction
from django.db.models import Count, Q
from django.forms import modelformset_factory
from django.forms import ModelChoiceField, ModelForm, modelformset_factory
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import escape
from django.utils.http import urlencode
from django.utils.http import is_safe_url, urlencode
from django.utils.safestring import mark_safe
from django.views.generic import View
from natsort import natsorted
from circuits.models import Circuit
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from extras.views import ObjectConfigContextView
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
from ipam.models import Prefix, Service, VLAN
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import (
@@ -39,21 +37,23 @@ from .models import (
)
class BulkRenameView(GetReturnURLMixin, View):
class BulkRenameView(View):
"""
An extendable view for renaming device components in bulk.
"""
queryset = None
model = None
form = None
template_name = 'dcim/bulk_rename.html'
def post(self, request):
model = self.queryset.model
return_url = request.GET.get('return_url')
if not return_url or not is_safe_url(url=return_url, host=request.get_host()):
return_url = 'home'
if '_preview' in request.POST or '_apply' in request.POST:
form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')})
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
if form.is_valid():
for obj in selected_objects:
@@ -65,19 +65,19 @@ class BulkRenameView(GetReturnURLMixin, View):
obj.save()
messages.success(request, "Renamed {} {}".format(
len(selected_objects),
model._meta.verbose_name_plural
self.model._meta.verbose_name_plural
))
return redirect(self.get_return_url(request))
return redirect(return_url)
else:
form = self.form(initial={'pk': request.POST.getlist('pk')})
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
return render(request, self.template_name, {
'form': form,
'obj_type_plural': model._meta.verbose_name_plural,
'obj_type_plural': self.model._meta.verbose_name_plural,
'selected_objects': selected_objects,
'return_url': self.get_return_url(request),
'return_url': return_url,
})
@@ -135,7 +135,9 @@ class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_region'
model = Region
model_form = forms.RegionForm
default_return_url = 'dcim:region_list'
def get_return_url(self, request, obj):
return reverse('dcim:region_list')
class RegionEditView(RegionCreateView):
@@ -151,8 +153,8 @@ class RegionBulkImportView(PermissionRequiredMixin, BulkImportView):
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region'
cls = Region
queryset = Region.objects.annotate(site_count=Count('sites'))
filter = filters.RegionFilter
table = tables.RegionTable
default_return_url = 'dcim:region_list'
@@ -222,6 +224,7 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_site'
cls = Site
queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter
table = tables.SiteTable
@@ -245,7 +248,9 @@ class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackgroup'
model = RackGroup
model_form = forms.RackGroupForm
default_return_url = 'dcim:rackgroup_list'
def get_return_url(self, request, obj):
return reverse('dcim:rackgroup_list')
class RackGroupEditView(RackGroupCreateView):
@@ -261,6 +266,7 @@ class RackGroupBulkImportView(PermissionRequiredMixin, BulkImportView):
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackgroup'
cls = RackGroup
queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
filter = filters.RackGroupFilter
table = tables.RackGroupTable
@@ -281,7 +287,9 @@ class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackrole'
model = RackRole
model_form = forms.RackRoleForm
default_return_url = 'dcim:rackrole_list'
def get_return_url(self, request, obj):
return reverse('dcim:rackrole_list')
class RackRoleEditView(RackRoleCreateView):
@@ -297,6 +305,7 @@ class RackRoleBulkImportView(PermissionRequiredMixin, BulkImportView):
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackrole'
cls = RackRole
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
table = tables.RackRoleTable
default_return_url = 'dcim:rackrole_list'
@@ -411,6 +420,7 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rack'
cls = Rack
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.RackFilter
table = tables.RackTable
@@ -420,6 +430,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rack'
cls = Rack
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.RackFilter
table = tables.RackTable
@@ -467,6 +478,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rackreservation'
cls = RackReservation
queryset = RackReservation.objects.select_related('rack', 'user')
filter = filters.RackReservationFilter
table = tables.RackReservationTable
@@ -476,8 +488,7 @@ class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackreservation'
queryset = RackReservation.objects.select_related('rack', 'user')
filter = filters.RackReservationFilter
cls = RackReservation
table = tables.RackReservationTable
default_return_url = 'dcim:rackreservation_list'
@@ -499,7 +510,9 @@ class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_manufacturer'
model = Manufacturer
model_form = forms.ManufacturerForm
default_return_url = 'dcim:manufacturer_list'
def get_return_url(self, request, obj):
return reverse('dcim:manufacturer_list')
class ManufacturerEditView(ManufacturerCreateView):
@@ -515,6 +528,7 @@ class ManufacturerBulkImportView(PermissionRequiredMixin, BulkImportView):
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_manufacturer'
cls = Manufacturer
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
table = tables.ManufacturerTable
default_return_url = 'dcim:manufacturer_list'
@@ -611,6 +625,7 @@ class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_devicetype'
cls = DeviceType
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
filter = filters.DeviceTypeFilter
table = tables.DeviceTypeTable
@@ -620,6 +635,7 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicetype'
cls = DeviceType
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
filter = filters.DeviceTypeFilter
table = tables.DeviceTypeTable
@@ -642,8 +658,10 @@ class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleporttemplate'
queryset = ConsolePortTemplate.objects.all()
parent_model = DeviceType
parent_field = 'device_type'
cls = ConsolePortTemplate
parent_cls = DeviceType
table = tables.ConsolePortTemplateTable
@@ -659,8 +677,8 @@ class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCrea
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverporttemplate'
queryset = ConsoleServerPortTemplate.objects.all()
parent_model = DeviceType
cls = ConsoleServerPortTemplate
parent_cls = DeviceType
table = tables.ConsoleServerPortTemplateTable
@@ -676,8 +694,8 @@ class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerporttemplate'
queryset = PowerPortTemplate.objects.all()
parent_model = DeviceType
cls = PowerPortTemplate
parent_cls = DeviceType
table = tables.PowerPortTemplateTable
@@ -693,8 +711,8 @@ class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlettemplate'
queryset = PowerOutletTemplate.objects.all()
parent_model = DeviceType
cls = PowerOutletTemplate
parent_cls = DeviceType
table = tables.PowerOutletTemplateTable
@@ -710,16 +728,16 @@ class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interfacetemplate'
queryset = InterfaceTemplate.objects.all()
parent_model = DeviceType
cls = InterfaceTemplate
parent_cls = DeviceType
table = tables.InterfaceTemplateTable
form = forms.InterfaceTemplateBulkEditForm
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interfacetemplate'
queryset = InterfaceTemplate.objects.all()
parent_model = DeviceType
cls = InterfaceTemplate
parent_cls = DeviceType
table = tables.InterfaceTemplateTable
@@ -735,8 +753,8 @@ class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebaytemplate'
queryset = DeviceBayTemplate.objects.all()
parent_model = DeviceType
cls = DeviceBayTemplate
parent_cls = DeviceType
table = tables.DeviceBayTemplateTable
@@ -754,7 +772,9 @@ class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_devicerole'
model = DeviceRole
model_form = forms.DeviceRoleForm
default_return_url = 'dcim:devicerole_list'
def get_return_url(self, request, obj):
return reverse('dcim:devicerole_list')
class DeviceRoleEditView(DeviceRoleCreateView):
@@ -770,7 +790,8 @@ class DeviceRoleBulkImportView(PermissionRequiredMixin, BulkImportView):
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicerole'
queryset = DeviceRole.objects.all()
cls = DeviceRole
queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
table = tables.DeviceRoleTable
default_return_url = 'dcim:devicerole_list'
@@ -789,7 +810,9 @@ class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_platform'
model = Platform
model_form = forms.PlatformForm
default_return_url = 'dcim:platform_list'
def get_return_url(self, request, obj):
return reverse('dcim:platform_list')
class PlatformEditView(PlatformCreateView):
@@ -805,7 +828,8 @@ class PlatformBulkImportView(PermissionRequiredMixin, BulkImportView):
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_platform'
queryset = Platform.objects.all()
cls = Platform
queryset = Platform.objects.annotate(device_count=Count('devices'))
table = tables.PlatformTable
default_return_url = 'dcim:platform_list'
@@ -833,7 +857,7 @@ class DeviceView(View):
# VirtualChassis members
if device.virtual_chassis is not None:
vc_members = Device.objects.filter(virtual_chassis=device.virtual_chassis).order_by('vc_position')
vc_members = Device.objects.filter(virtual_chassis=device.virtual_chassis)
else:
vc_members = []
@@ -858,10 +882,8 @@ class DeviceView(View):
device.device_type.interface_ordering
).select_related(
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
'circuit_termination__circuit__provider'
).prefetch_related(
'tags', 'ip_addresses'
)
'circuit_termination__circuit'
).prefetch_related('ip_addresses')
# Device bays
device_bays = natsorted(
@@ -919,7 +941,6 @@ class DeviceInventoryView(View):
return render(request, 'dcim/device_inventory.html', {
'device': device,
'inventory_items': inventory_items,
'active_tab': 'inventory',
})
@@ -932,7 +953,6 @@ class DeviceStatusView(PermissionRequiredMixin, View):
return render(request, 'dcim/device_status.html', {
'device': device,
'active_tab': 'status',
})
@@ -942,16 +962,17 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
def get(self, request, pk):
device = get_object_or_404(Device, pk=pk)
interfaces = device.vc_interfaces.order_naturally(
interfaces = Interface.objects.order_naturally(
device.device_type.interface_ordering
).connectable().select_related(
).connectable().filter(
device=device
).select_related(
'connected_as_a', 'connected_as_b'
)
return render(request, 'dcim/device_lldp_neighbors.html', {
'device': device,
'interfaces': interfaces,
'active_tab': 'lldp-neighbors',
})
@@ -964,15 +985,9 @@ class DeviceConfigView(PermissionRequiredMixin, View):
return render(request, 'dcim/device_config.html', {
'device': device,
'active_tab': 'config',
})
class DeviceConfigContextView(ObjectConfigContextView):
object_class = Device
base_template = 'dcim/device.html'
class DeviceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_device'
model = Device
@@ -1020,6 +1035,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_device'
cls = Device
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
filter = filters.DeviceFilter
table = tables.DeviceTable
@@ -1029,6 +1045,7 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_device'
cls = Device
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
filter = filters.DeviceFilter
table = tables.DeviceTable
@@ -1085,6 +1102,7 @@ class ConsolePortConnectView(PermissionRequiredMixin, View):
escape(consoleport.cs_port.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, consoleport, msg)
return redirect('dcim:device', pk=consoleport.device.pk)
@@ -1135,6 +1153,7 @@ class ConsolePortDisconnectView(PermissionRequiredMixin, View):
escape(cs_port.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, consoleport, msg)
return redirect('dcim:device', pk=consoleport.device.pk)
@@ -1158,8 +1177,8 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport'
queryset = ConsolePort.objects.all()
parent_model = Device
cls = ConsolePort
parent_cls = Device
table = tables.ConsolePortTable
@@ -1223,6 +1242,7 @@ class ConsoleServerPortConnectView(PermissionRequiredMixin, View):
escape(consoleserverport.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, consoleport, msg)
return redirect('dcim:device', pk=consoleserverport.device.pk)
@@ -1274,6 +1294,7 @@ class ConsoleServerPortDisconnectView(PermissionRequiredMixin, View):
escape(consoleserverport.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, consoleport, msg)
return redirect('dcim:device', pk=consoleserverport.device.pk)
@@ -1297,7 +1318,7 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class ConsoleServerPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_consoleserverport'
queryset = ConsoleServerPort.objects.all()
model = ConsoleServerPort
form = forms.ConsoleServerPortBulkRenameForm
@@ -1312,8 +1333,8 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport'
queryset = ConsoleServerPort.objects.all()
parent_model = Device
cls = ConsoleServerPort
parent_cls = Device
table = tables.ConsoleServerPortTable
@@ -1367,6 +1388,7 @@ class PowerPortConnectView(PermissionRequiredMixin, View):
escape(powerport.power_outlet.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, powerport, msg)
return redirect('dcim:device', pk=powerport.device.pk)
@@ -1417,6 +1439,7 @@ class PowerPortDisconnectView(PermissionRequiredMixin, View):
escape(power_outlet.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, powerport, msg)
return redirect('dcim:device', pk=powerport.device.pk)
@@ -1440,8 +1463,8 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport'
queryset = PowerPort.objects.all()
parent_model = Device
cls = PowerPort
parent_cls = Device
table = tables.PowerPortTable
@@ -1504,6 +1527,7 @@ class PowerOutletConnectView(PermissionRequiredMixin, View):
escape(poweroutlet.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, powerport, msg)
return redirect('dcim:device', pk=poweroutlet.device.pk)
@@ -1554,6 +1578,7 @@ class PowerOutletDisconnectView(PermissionRequiredMixin, View):
escape(poweroutlet.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, powerport, msg)
return redirect('dcim:device', pk=poweroutlet.device.pk)
@@ -1577,7 +1602,7 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class PowerOutletBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_poweroutlet'
queryset = PowerOutlet.objects.all()
model = PowerOutlet
form = forms.PowerOutletBulkRenameForm
@@ -1594,8 +1619,8 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView)
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet'
queryset = PowerOutlet.objects.all()
parent_model = Device
cls = PowerOutlet
parent_cls = Device
table = tables.PowerOutletTable
@@ -1603,47 +1628,6 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Interfaces
#
class InterfaceView(View):
def get(self, request, pk):
interface = get_object_or_404(Interface, pk=pk)
# Get connected interface
connected_interface = interface.connected_interface
if connected_interface is None and hasattr(interface, 'circuit_termination'):
peer_termination = interface.circuit_termination.get_peer_termination()
if peer_termination is not None:
connected_interface = peer_termination.interface
# Get assigned IP addresses
ipaddress_table = InterfaceIPAddressTable(
data=interface.ip_addresses.select_related('vrf', 'tenant'),
orderable=False
)
# Get assigned VLANs and annotate whether each is tagged or untagged
vlans = []
if interface.untagged_vlan is not None:
vlans.append(interface.untagged_vlan)
vlans[0].tagged = False
for vlan in interface.tagged_vlans.select_related('site', 'group', 'tenant', 'role'):
vlan.tagged = True
vlans.append(vlan)
vlan_table = InterfaceVLANTable(
interface=interface,
data=vlans,
orderable=False
)
return render(request, 'dcim/interface.html', {
'interface': interface,
'connected_interface': connected_interface,
'ipaddress_table': ipaddress_table,
'vlan_table': vlan_table,
})
class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interface'
parent_model = Device
@@ -1661,12 +1645,6 @@ class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
template_name = 'dcim/interface_edit.html'
class InterfaceAssignVLANsView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_interface'
model = Interface
model_form = forms.InterfaceAssignVLANsForm
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_interface'
model = Interface
@@ -1686,22 +1664,22 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface'
queryset = Interface.objects.all()
parent_model = Device
cls = Interface
parent_cls = Device
table = tables.InterfaceTable
form = forms.InterfaceBulkEditForm
class InterfaceBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_interface'
queryset = Interface.objects.order_naturally()
model = Interface
form = forms.InterfaceBulkRenameForm
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interface'
queryset = Interface.objects.all()
parent_model = Device
cls = Interface
parent_cls = Device
table = tables.InterfaceTable
@@ -1801,14 +1779,14 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View):
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_devicebay'
queryset = DeviceBay.objects.all()
model = DeviceBay
form = forms.DeviceBayBulkRenameForm
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebay'
queryset = DeviceBay.objects.all()
parent_model = Device
cls = DeviceBay
parent_cls = Device
table = tables.DeviceBayTable
@@ -1924,6 +1902,7 @@ class InterfaceConnectionAddView(PermissionRequiredMixin, GetReturnURLMixin, Vie
escape(interfaceconnection.interface_b.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
if '_addanother' in request.POST:
base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
@@ -1974,6 +1953,7 @@ class InterfaceConnectionDeleteView(PermissionRequiredMixin, GetReturnURLMixin,
escape(interfaceconnection.interface_b.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
return redirect(self.get_return_url(request, interfaceconnection))
@@ -2065,6 +2045,7 @@ class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView):
class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_inventoryitem'
cls = InventoryItem
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
filter = filters.InventoryItemFilter
table = tables.InventoryItemTable
@@ -2074,6 +2055,7 @@ class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_inventoryitem'
cls = InventoryItem
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_bulk_delete.html'
@@ -2085,10 +2067,8 @@ class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class VirtualChassisListView(ObjectListView):
queryset = VirtualChassis.objects.select_related('master').annotate(member_count=Count('members'))
queryset = VirtualChassis.objects.annotate(member_count=Count('members'))
table = tables.VirtualChassisTable
filter = filters.VirtualChassisFilter
filter_form = forms.VirtualChassisFilterForm
template_name = 'dcim/virtualchassis_list.html'
@@ -2100,25 +2080,20 @@ class VirtualChassisCreateView(PermissionRequiredMixin, View):
# Get the list of devices being added to a VirtualChassis
pk_form = forms.DeviceSelectionForm(request.POST)
pk_form.full_clean()
if not pk_form.cleaned_data.get('pk'):
device_list = pk_form.cleaned_data.get('pk')
if not device_list:
messages.warning(request, "No devices were selected.")
return redirect('dcim:device_list')
device_queryset = Device.objects.filter(
pk__in=pk_form.cleaned_data.get('pk')
).select_related('rack').order_by('vc_position')
VCMemberFormSet = modelformset_factory(
model=Device,
formset=forms.BaseVCMemberFormSet,
form=forms.DeviceVCMembershipForm,
extra=0
)
# TODO: Error if any of the devices already belong to a VC
VCMemberFormSet = modelformset_factory(model=Device, fields=('vc_position', 'vc_priority'), extra=0)
if '_create' in request.POST:
vc_form = forms.VirtualChassisForm(request.POST)
vc_form.fields['master'].queryset = device_queryset
formset = VCMemberFormSet(request.POST, queryset=device_queryset)
formset = VCMemberFormSet(request.POST)
if vc_form.is_valid() and formset.is_valid():
@@ -2136,8 +2111,8 @@ class VirtualChassisCreateView(PermissionRequiredMixin, View):
else:
vc_form = forms.VirtualChassisForm()
vc_form.fields['master'].queryset = device_queryset
formset = VCMemberFormSet(queryset=device_queryset)
vc_form.fields['master'].queryset = Device.objects.filter(pk__in=device_list)
formset = VCMemberFormSet(queryset=Device.objects.filter(pk__in=device_list))
return render(request, 'dcim/virtualchassis_edit.html', {
'pk_form': pk_form,
@@ -2153,17 +2128,11 @@ class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
def get(self, request, pk):
virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
VCMemberFormSet = modelformset_factory(
model=Device,
form=forms.DeviceVCMembershipForm,
formset=forms.BaseVCMemberFormSet,
extra=0
)
members_queryset = virtual_chassis.members.select_related('rack').order_by('vc_position')
VCMemberFormSet = modelformset_factory(model=Device, fields=('vc_position', 'vc_priority'), extra=0)
vc_form = forms.VirtualChassisForm(instance=virtual_chassis)
vc_form.fields['master'].queryset = members_queryset
formset = VCMemberFormSet(queryset=members_queryset)
vc_form.fields['master'].queryset = virtual_chassis.members.all()
formset = VCMemberFormSet(queryset=virtual_chassis.members.all())
return render(request, 'dcim/virtualchassis_edit.html', {
'vc_form': vc_form,
@@ -2174,17 +2143,11 @@ class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
def post(self, request, pk):
virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
VCMemberFormSet = modelformset_factory(
model=Device,
form=forms.DeviceVCMembershipForm,
formset=forms.BaseVCMemberFormSet,
extra=0
)
members_queryset = virtual_chassis.members.select_related('rack').order_by('vc_position')
VCMemberFormSet = modelformset_factory(model=Device, fields=('vc_position', 'vc_priority'), extra=0)
vc_form = forms.VirtualChassisForm(request.POST, instance=virtual_chassis)
vc_form.fields['master'].queryset = members_queryset
formset = VCMemberFormSet(request.POST, queryset=members_queryset)
vc_form.fields['master'].queryset = virtual_chassis.members.all()
formset = VCMemberFormSet(request.POST, queryset=virtual_chassis.members.all())
if vc_form.is_valid() and formset.is_valid():
@@ -2244,13 +2207,14 @@ class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, Vi
device = member_select_form.cleaned_data['device']
device.virtual_chassis = virtual_chassis
data = {k: request.POST[k] for k in ['vc_position', 'vc_priority']}
membership_form = forms.DeviceVCMembershipForm(data=data, validate_vc_position=True, instance=device)
membership_form = forms.DeviceVCMembershipForm(data, instance=device)
if membership_form.is_valid():
membership_form.save()
msg = 'Added member <a href="{}">{}</a>'.format(device.get_absolute_url(), escape(device))
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, device, msg)
if '_addanother' in request.POST:
return redirect(request.get_full_path())
@@ -2259,7 +2223,7 @@ class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, Vi
else:
membership_form = forms.DeviceVCMembershipForm(data=request.POST)
membership_form = forms.DeviceVCMembershipForm(request.POST)
return render(request, 'dcim/virtualchassis_add_member.html', {
'virtual_chassis': virtual_chassis,
@@ -2305,6 +2269,7 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
msg = 'Removed {} from virtual chassis {}'.format(device, device.virtual_chassis)
messages.success(request, msg)
UserAction.objects.log_edit(request.user, device, msg)
return redirect(self.get_return_url(request, device))

View File

@@ -1,15 +0,0 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
default_app_config = 'extras.apps.ExtrasConfig'
# check that django-rq is installed and we can connect to redis
if settings.WEBHOOKS_ENABLED:
try:
import django_rq
except ImportError:
raise ImproperlyConfigured(
"django-rq is not installed! You must install this package per "
"the documentation to use the webhook backend."
)

View File

@@ -4,9 +4,7 @@ from django import forms
from django.contrib import admin
from django.utils.safestring import mark_safe
from netbox.admin import admin_site
from utilities.forms import LaxURLField
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction, Webhook
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction
def order_content_types(field):
@@ -17,37 +15,6 @@ def order_content_types(field):
field.choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
#
# Webhooks
#
class WebhookForm(forms.ModelForm):
payload_url = LaxURLField(
label='URL'
)
class Meta:
model = Webhook
exclude = []
def __init__(self, *args, **kwargs):
super(WebhookForm, self).__init__(*args, **kwargs)
order_content_types(self.fields['obj_type'])
@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',
]
form = WebhookForm
def models(self, obj):
return ', '.join([ct.name for ct in obj.obj_type.all()])
#
# Custom fields
#
@@ -69,10 +36,10 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
extra = 5
@admin.register(CustomField, site=admin_site)
@admin.register(CustomField)
class CustomFieldAdmin(admin.ModelAdmin):
inlines = [CustomFieldChoiceAdmin]
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
list_display = ['name', 'models', 'type', 'required', 'default', 'weight', 'description']
form = CustomFieldForm
def models(self, obj):
@@ -83,7 +50,7 @@ class CustomFieldAdmin(admin.ModelAdmin):
# Graphs
#
@admin.register(Graph, site=admin_site)
@admin.register(Graph)
class GraphAdmin(admin.ModelAdmin):
list_display = ['name', 'type', 'weight', 'source']
@@ -106,7 +73,7 @@ class ExportTemplateForm(forms.ModelForm):
self.fields['content_type'].choices.insert(0, ('', '---------'))
@admin.register(ExportTemplate, site=admin_site)
@admin.register(ExportTemplate)
class ExportTemplateAdmin(admin.ModelAdmin):
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
form = ExportTemplateForm
@@ -116,7 +83,7 @@ class ExportTemplateAdmin(admin.ModelAdmin):
# Topology maps
#
@admin.register(TopologyMap, site=admin_site)
@admin.register(TopologyMap)
class TopologyMapAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'site']
prepopulated_fields = {
@@ -128,7 +95,7 @@ class TopologyMapAdmin(admin.ModelAdmin):
# User actions
#
@admin.register(UserAction, site=admin_site)
@admin.register(UserAction)
class UserActionAdmin(admin.ModelAdmin):
actions = None
list_display = ['user', 'action', 'content_type', 'object_id', '_message']

View File

@@ -2,31 +2,28 @@ from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from taggit.models import Tag
from dcim.api.serializers import (
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
NestedRegionSerializer, NestedSiteSerializer,
)
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
from extras.models import (
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction,
)
from extras.constants import *
from tenancy.api.serializers import NestedTenantSerializer, NestedTenantGroupSerializer
from tenancy.models import Tenant, TenantGroup
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
from dcim.models import Device, Rack, Site
from extras.constants import ACTION_CHOICES, GRAPH_TYPE_CHOICES
from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
from users.api.serializers import NestedUserSerializer
from utilities.api import (
ChoiceField, ContentTypeField, get_serializer_for_model, SerializedPKRelatedField, ValidatedModelSerializer,
)
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
#
# Graphs
#
class GraphSerializer(ValidatedModelSerializer):
type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
class GraphSerializer(serializers.ModelSerializer):
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
class Meta:
model = Graph
fields = ['id', 'type', 'weight', 'name', 'source', 'link']
class WritableGraphSerializer(serializers.ModelSerializer):
class Meta:
model = Graph
@@ -36,7 +33,7 @@ class GraphSerializer(ValidatedModelSerializer):
class RenderedGraphSerializer(serializers.ModelSerializer):
embed_url = serializers.SerializerMethodField()
embed_link = serializers.SerializerMethodField()
type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
class Meta:
model = Graph
@@ -53,7 +50,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
# Export templates
#
class ExportTemplateSerializer(ValidatedModelSerializer):
class ExportTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = ExportTemplate
@@ -64,7 +61,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
# Topology maps
#
class TopologyMapSerializer(ValidatedModelSerializer):
class TopologyMapSerializer(serializers.ModelSerializer):
site = NestedSiteSerializer()
class Meta:
@@ -72,46 +69,23 @@ class TopologyMapSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
#
# Tags
#
class TagSerializer(ValidatedModelSerializer):
tagged_items = serializers.IntegerField(read_only=True)
class WritableTopologyMapSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'slug', 'tagged_items']
model = TopologyMap
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
#
# Image attachments
#
class ImageAttachmentSerializer(ValidatedModelSerializer):
content_type = ContentTypeField()
parent = serializers.SerializerMethodField(read_only=True)
class ImageAttachmentSerializer(serializers.ModelSerializer):
parent = serializers.SerializerMethodField()
class Meta:
model = ImageAttachment
fields = [
'id', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created',
]
def validate(self, data):
# Validate that the parent object exists
try:
data['content_type'].get_object_for_this_type(id=data['object_id'])
except ObjectDoesNotExist:
raise serializers.ValidationError(
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
)
# Enforce model validation
super(ImageAttachmentSerializer, self).validate(data)
return data
fields = ['id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created']
def get_parent(self, obj):
@@ -128,54 +102,27 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
return serializer(obj.parent, context={'request': self.context['request']}).data
#
# Config contexts
#
class ConfigContextSerializer(ValidatedModelSerializer):
regions = SerializedPKRelatedField(
queryset=Region.objects.all(),
serializer=NestedRegionSerializer,
required=False,
many=True
)
sites = SerializedPKRelatedField(
queryset=Site.objects.all(),
serializer=NestedSiteSerializer,
required=False,
many=True
)
roles = SerializedPKRelatedField(
queryset=DeviceRole.objects.all(),
serializer=NestedDeviceRoleSerializer,
required=False,
many=True
)
platforms = SerializedPKRelatedField(
queryset=Platform.objects.all(),
serializer=NestedPlatformSerializer,
required=False,
many=True
)
tenant_groups = SerializedPKRelatedField(
queryset=TenantGroup.objects.all(),
serializer=NestedTenantGroupSerializer,
required=False,
many=True
)
tenants = SerializedPKRelatedField(
queryset=Tenant.objects.all(),
serializer=NestedTenantSerializer,
required=False,
many=True
)
class WritableImageAttachmentSerializer(ValidatedModelSerializer):
content_type = ContentTypeFieldSerializer()
class Meta:
model = ConfigContext
fields = [
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
'tenant_groups', 'tenants', 'data',
]
model = ImageAttachment
fields = ['id', 'content_type', 'object_id', 'name', 'image']
def validate(self, data):
# Validate that the parent object exists
try:
data['content_type'].get_object_for_this_type(id=data['object_id'])
except ObjectDoesNotExist:
raise serializers.ValidationError(
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
)
# Enforce model validation
super(WritableImageAttachmentSerializer, self).validate(data)
return data
#
@@ -213,42 +160,13 @@ class ReportDetailSerializer(ReportSerializer):
result = ReportResultSerializer()
#
# Change logging
#
class ObjectChangeSerializer(serializers.ModelSerializer):
user = NestedUserSerializer(read_only=True)
content_type = ContentTypeField(read_only=True)
changed_object = serializers.SerializerMethodField(read_only=True)
class Meta:
model = ObjectChange
fields = [
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'content_type', 'changed_object', 'object_data',
]
def get_changed_object(self, obj):
"""
Serialize a nested representation of the changed object.
"""
if obj.changed_object is None:
return None
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
if serializer is None:
return obj.object_repr
context = {'request': self.context['request']}
data = serializer(obj.changed_object, context=context).data
return data
#
# User actions
#
class UserActionSerializer(serializers.ModelSerializer):
user = NestedUserSerializer()
action = ChoiceField(choices=ACTION_CHOICES)
action = ChoiceFieldSerializer(choices=ACTION_CHOICES)
class Meta:
model = UserAction

View File

@@ -28,21 +28,12 @@ router.register(r'export-templates', views.ExportTemplateViewSet)
# Topology maps
router.register(r'topology-maps', views.TopologyMapViewSet)
# Tags
router.register(r'tags', views.TagViewSet)
# Image attachments
router.register(r'image-attachments', views.ImageAttachmentViewSet)
# Config contexts
router.register(r'config-contexts', views.ConfigContextViewSet)
# Reports
router.register(r'reports', views.ReportViewSet, base_name='report')
# Change logging
router.register(r'object-changes', views.ObjectChangeViewSet)
# Recent activity
router.register(r'recent-activity', views.RecentActivityViewSet)

View File

@@ -1,20 +1,15 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from taggit.models import Tag
from extras import filters
from extras.models import (
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
UserAction,
)
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
from extras.reports import get_report, get_reports
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
from . import serializers
@@ -72,6 +67,7 @@ class CustomFieldModelViewSet(ModelViewSet):
class GraphViewSet(ModelViewSet):
queryset = Graph.objects.all()
serializer_class = serializers.GraphSerializer
write_serializer_class = serializers.WritableGraphSerializer
filter_class = filters.GraphFilter
@@ -92,9 +88,10 @@ class ExportTemplateViewSet(ModelViewSet):
class TopologyMapViewSet(ModelViewSet):
queryset = TopologyMap.objects.select_related('site')
serializer_class = serializers.TopologyMapSerializer
write_serializer_class = serializers.WritableTopologyMapSerializer
filter_class = filters.TopologyMapFilter
@action(detail=True)
@detail_route()
def render(self, request, pk):
tmap = get_object_or_404(TopologyMap, pk=pk)
@@ -102,7 +99,7 @@ class TopologyMapViewSet(ModelViewSet):
try:
data = tmap.render(img_format=img_format)
except Exception:
except:
return HttpResponse(
"There was an error generating the requested graph. Ensure that the GraphViz executables have been "
"installed correctly."
@@ -114,16 +111,6 @@ class TopologyMapViewSet(ModelViewSet):
return response
#
# Tags
#
class TagViewSet(ModelViewSet):
queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items'))
serializer_class = serializers.TagSerializer
filter_class = filters.TagFilter
#
# Image attachments
#
@@ -131,18 +118,7 @@ class TagViewSet(ModelViewSet):
class ImageAttachmentViewSet(ModelViewSet):
queryset = ImageAttachment.objects.all()
serializer_class = serializers.ImageAttachmentSerializer
#
# Config contexts
#
class ConfigContextViewSet(ModelViewSet):
queryset = ConfigContext.objects.prefetch_related(
'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
)
serializer_class = serializers.ConfigContextSerializer
filter_class = filters.ConfigContextFilter
write_serializer_class = serializers.WritableImageAttachmentSerializer
#
@@ -202,7 +178,7 @@ class ReportViewSet(ViewSet):
return Response(serializer.data)
@action(detail=True, methods=['post'])
@detail_route(methods=['post'])
def run(self, request, pk):
"""
Run a Report and create a new ReportResult, overwriting any previous result for the Report.
@@ -221,26 +197,13 @@ class ReportViewSet(ViewSet):
return Response(serializer.data)
#
# Change logging
#
class ObjectChangeViewSet(ReadOnlyModelViewSet):
"""
Retrieve a list of recent changes.
"""
queryset = ObjectChange.objects.select_related('user')
serializer_class = serializers.ObjectChangeSerializer
filter_class = filters.ObjectChangeFilter
#
# User activity
#
class RecentActivityViewSet(ReadOnlyModelViewSet):
"""
DEPRECATED: List all UserActions to provide a log of recent activity.
List all UserActions to provide a log of recent activity.
"""
queryset = UserAction.objects.all()
serializer_class = serializers.UserActionSerializer

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