Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
1e04bd3c79 Release v2.1-beta 2017-07-10 13:15:52 -04:00
292 changed files with 3125 additions and 9441 deletions

View File

@@ -1,48 +1,28 @@
<!-- <!--
Before opening a new issue, please search through the existing issues to Please note: GitHub issues are to be used only for feature requests
see if your topic has already been addressed. Note that you may need to and bug reports. For installation assistance or general discussion,
remove the "is:open" filter from the search bar to include closed issues. please join us on the mailing list:
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 https://groups.google.com/forum/#!forum/netbox-discuss
Please note that issues which do not fall under any of the below categories Please indicate "bug report" or "feature request" below. Be sure to
will be closed. Due to an excessive backlog of feature requests, we are search the existing set of issues (both open and closed) to see if
not currently accepting any proposals which extend NetBox's feature scope. a similar issue has already been raised.
-->
Do not prepend any sort of tag to your issue's title. An administrator will ### Issue type:
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 If filing a bug, please indicate the version of Python and NetBox
to verify that you are running the latest stable release of NetBox before you are running. (This is not necessary for feature requests.)
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:**
* Python version: <!-- Example: 3.5.4 --> **NetBox version:**
* NetBox version: <!-- Example: 2.1.3 -->
<!-- <!--
BUG REPORTS must include: If filing a bug, please record the exact steps taken to reproduce
* A list of the steps needed for someone else to reproduce the bug the bug and any errors messages that are generated.
* A description of the expected and observed behavior
* Any relevant error messages (screenshots may also help)
FEATURE REQUESTS must include: If filing a feature request, please precisely describe the data
* A detailed description of the proposed functionality model or workflow you would like to see implemented, and provide a
* A use case for the new feature use case.
* A rough description of any necessary changes to the database schema
* Any relevant third-party libraries which would be needed
--> -->
### Description

2
.gitignore vendored
View File

@@ -1,8 +1,6 @@
*.pyc *.pyc
/netbox/netbox/configuration.py /netbox/netbox/configuration.py
/netbox/netbox/ldap_config.py /netbox/netbox/ldap_config.py
/netbox/reports/*
!/netbox/reports/__init__.py
/netbox/static /netbox/static
.idea .idea
/*.sh /*.sh

View File

@@ -1,8 +1,11 @@
sudo: required sudo: required
services: services:
- postgresql - docker
addons:
postgresql: "9.4" env:
- DOCKER_TAG=$TRAVIS_TAG
language: python language: python
python: python:
- "2.7" - "2.7"
@@ -10,8 +13,9 @@ python:
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install pep8 - pip install pep8
before_script:
- psql --version
- psql -U postgres -c 'SELECT version();'
script: script:
- ./scripts/cibuild.sh - ./scripts/cibuild.sh
after_success:
- if [ ! -z "$TRAVIS_TAG" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then
./scripts/docker-build.sh;
fi

View File

@@ -1,8 +1,8 @@
## Getting Help ## Getting Help
If you encounter any issues installing or using NetBox, try one of the If you encounter any issues installing or using NetBox, try one of the
following resources to get assistance. Please **do not** open a GitHub issue following resources to get assistance. Please **do not** open a GitHub
except to report bugs or request features. issue except to report bugs or request features.
### Mailing List ### Mailing List
@@ -13,32 +13,35 @@ installation. You can find us [here](https://groups.google.com/forum/#!forum/net
### Freenode IRC ### Freenode IRC
For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/). For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/).
You can connect to Freenode at irc.freenode.net using an IRC client, or you can You can connect to Freenode at irc.freenode.net using an IRC client, or
use their [webchat client](https://webchat.freenode.net/). you can use their [webchat client](https://webchat.freenode.net/).
## Reporting Bugs ## Reporting Bugs
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of * First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
NetBox. If you're running an older version, it's possible that the bug has NetBox. If you're running an older version, it's possible that the bug
already been fixed. has already been fixed.
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has already * Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has
been reported. If you think you may be experiencing a reported issue that already been reported. If you think you may be experiencing a reported
hasn't already been resolved, please click "add a reaction" in the top right issue that hasn't already been resolved, please click "add a reaction"
corner of the issue and add a thumbs up (+1). You mightalso want to add a in the top right corner of the issue and add a thumbs up (+1). You might
comment describing how it's affecting your installation. This will allow us to also want to add a comment describing how it's affecting your
prioritize bugs based on how many users are affected. installation. This will allow us to prioritize bugs based on how many
users are affected.
* If you haven't found an existing issue that describes your suspected bug, * If you haven't found an existing issue that describes your suspected
please inquire about it on the mailing list. **Do not** file an issue until you bug, please inquire about it on the mailing list. **Do not** file an
have received confirmation that it is in fact a bug. Invalid issues are very issue until you have received confirmation that it is in fact a bug.
distracting and slow the pace at which NetBox is developed. Invalid issues are very distracting and slow the pace at which NetBox is
developed.
* When submitting an issue, please be as descriptive as possible. Be sure to * When submitting an issue, please be as descriptive as possible. Be
include: sure to include:
* The environment in which NetBox is running * The environment in which NetBox is running
* The exact steps that can be taken to reproduce the issue (if applicable) * The exact steps that can be taken to reproduce the issue (if
applicable)
* Any error messages generated * Any error messages generated
* Screenshots (if applicable) * Screenshots (if applicable)
@@ -46,64 +49,71 @@ include:
The issue will be reviewed by a moderator after submission and the appropriate The issue will be reviewed by a moderator after submission and the appropriate
labels will be applied. labels will be applied.
* Keep in mind that we prioritize bugs based on their severity and how much * Keep in mind that we prioritize bugs based on their severity and how
work is required to resolve them. It may take some time for someone to address much work is required to resolve them. It may take some time for someone
your issue. to address your issue.
## Feature Requests ## Feature Requests
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting * First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
is already listed. (Be sure to search closed issues as well, since some requesting is already listed. (Be sure to search closed issues as well,
feature requests have been rejected.) If the feature you'd like to see has since some feature requests are rejected.) If the feature you'd like to
already been requested and is open, click "add a reaction" in the top right see has already been requested, click "add a reaction" in the top right
corner of the issue and add a thumbs up (+1). This ensures that the issue has corner of the issue and add a thumbs up (+1). This ensures that the
a better chance of receiving attention. Also feel free to add a comment with issue has a better chance of making it onto the roadmap. Also feel free
any additional justification for the feature. (However, note that comments with to add a comment with any additional justification for the feature.
no substance other than a "+1" will be deleted. Please use GitHub's reactions (However, note that comments with no substance other than a "+1" will be
feature to indicate your support.) deleted. Please use GitHub's reactions feature to indicate your
support.)
* Due to an excessive backlog of feature requests, we are not currently * While suggestions for new features are welcome, it's important to
accepting any proposals which substantially extend NetBox's functionality limit the scope of NetBox's feature set to avoid feature creep. For
beyond its current feature set. This includes the introduction of any new views example, the following features would be firmly out of scope for NetBox:
or models which have not already been proposed in an existing feature request.
* Ticket management
* Network state monitoring
* Acting as a DNS server
* Acting as an authentication server
* Before filing a new feature request, consider raising your idea on the * Before filing a new feature request, consider raising your idea on the
mailing list first. Feedback you receive there will help validate and shape the mailing list first. Feedback you receive there will help validate and
proposed feature before filing a formal issue. shape the proposed feature before filing a formal issue.
* Good feature requests are very narrowly defined. Be sure to thoroughly * Good feature requests are very narrowly defined. Be sure to enumerate
describe the functionality and data model(s) being proposed. The more effort specific functionality and data schema. The more effort you put into
you put into writing a feature request, the better its chance is of being writing a feature request, the better its chance is of being
implemented. Overly broad feature requests will be closed. implemented. Overly broad feature requests will be closed.
* When submitting a feature request on GitHub, be sure to include the * When submitting a feature request on GitHub, be sure to include the
following: following:
* A detailed description of the proposed functionality * A detailed description of the proposed functionality
* A use case for the feature; who would use it and what value it would add * A use case for the feature; who would use it and what value it
to NetBox would add to NetBox
* A rough description of changes necessary to the database schema (if * A rough description of changes necessary to the database schema
applicable) (if applicable)
* Any third-party libraries or other resources which would be involved * Any third-party libraries or other resources which would be
involved
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue * Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title.
title. The issue will be reviewed by a moderator after submission and the The issue will be reviewed by a moderator after submission and the appropriate
appropriate labels will be applied. labels will be applied.
## Submitting Pull Requests ## Submitting Pull Requests
* Be sure to open an issue before starting work on a pull request, and discuss * Be sure to open an issue before starting work on a pull request, and
your idea with the NetBox maintainers before beginning work. This will help discuss your idea with the NetBox maintainers before beginning work.
prevent wasting time on something that might we might not be able to implement. This will help prevent wasting time on something that might we might not
When suggesting a new feature, also make sure it won't conflict with any work be able to implement. When suggesting a new feature, also make sure it
that's already in progress. 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` * When submitting a pull request, please be sure to work off of the
branch, rather than `master`. The `develop` branch is used for ongoing `develop` branch, rather than `master`. In NetBox, the `develop` branch
development, while `master` is used for tagging new stable releases. is used for ongoing development, while `master` is used for tagging new
stable releases.
* All code submissions should meet the following criteria (CI will enforce * All code submissions should meet the following criteria (CI will
these checks): enforce these checks):
* Python syntax is valid * Python syntax is valid
* All tests pass when run with `./manage.py test` * All tests pass when run with `./manage.py test`

View File

@@ -31,5 +31,6 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst
## Alternative Installations ## Alternative Installations
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine)) * [Docker container](https://github.com/digitalocean/netbox-docker)
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle)) * [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)

View File

@@ -4,8 +4,7 @@ Supported HTTP methods:
* `GET`: Retrieve an object or list of objects * `GET`: Retrieve an object or list of objects
* `POST`: Create a new object * `POST`: Create a new object
* `PUT`: Update an existing object, all mandatory fields must be specified * `PUT`: Update an existing object
* `PATCH`: Updates an existing object, only specifiying the field to be changed
* `DELETE`: Delete an existing object * `DELETE`: Delete an existing object
To authenticate a request, attach your token in an `Authorization` header: To authenticate a request, attach your token in an `Authorization` header:
@@ -105,19 +104,12 @@ $ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0
### Modify an existing site ### Modify an existing site
Make an authenticated `PUT` request to the site detail endpoint. As with a create (`POST`) request, all mandatory fields must be included. Make an authenticated `PUT` request to the site detail endpoint. As with a create (POST) request, all mandatory fields must be included.
``` ```
$ curl -X PUT -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/ --data '{"name": "Renamed Site", "slug": "renamed-site"}' $ curl -X PUT -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/ --data '{"name": "Renamed Site", "slug": "renamed-site"}'
``` ```
### Modify an object by changing a field
Make an authenticated `PATCH` request to the device endpoint. With `PATCH`, unlike `POST` and `PUT`, we only specify the field that is being changed. In this example, we add a serial number to a device.
```
$ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/devices/2549/ --data '{"serial": "FTX1123A090"}'
```
### Delete an existing site ### Delete an existing site
Send an authenticated `DELETE` request to the site detail endpoint. Send an authenticated `DELETE` request to the site detail endpoint.

View File

@@ -1,42 +1,8 @@
NetBox v2.0 and later includes a full-featured REST API that allows its data model to be read and manipulated externally. NetBox v2.0 and later includes a full-featured REST API that allows its data model to be read and manipulated externally.
# What is a REST API?
REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP to create, retrieve, update, and delete objects from a database. (This set of operations is commonly referred to as CRUD.) Each type of operation is associated with a particular HTTP verb:
* `GET`: Retrieve an object or list of objects
* `POST`: Create an object
* `PUT` / `PATCH`: Modify an existing object. `PUT` requires all mandatory fields to be specified, while `PATCH` only expects the field that is being modified to be specified.
* `DELETE`: Delete an existing object
The NetBox API represents all objects in [JavaScript Object Notation (JSON)](http://www.json.org/). This makes it very easy to interact with NetBox data on the command line with common tools. For example, we can request an IP address from NetBox and output the JSON using `curl` and `jq`. (Piping the output through `jq` isn't strictly required but makes it much easier to read.)
```
$ curl -s http://localhost:8000/api/ipam/ip-addresses/2954/ | jq '.'
{
"custom_fields": {},
"nat_outside": null,
"nat_inside": null,
"description": "An example IP address",
"id": 2954,
"family": 4,
"address": "5.101.108.132/26",
"vrf": null,
"tenant": null,
"status": {
"label": "Active",
"value": 1
},
"role": null,
"interface": null
}
```
Each attribute of the NetBox object is expressed as a field in the dictionary. Fields may include their own nested objects, as in the case of the `status` field above. Every object includes a primary key named `id` which uniquely identifies it in the database.
# URL Hierarchy # URL Hierarchy
NetBox's entire API is housed under the API root at `https://<hostname>/api/`. The URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example, the provider and circuit objects are located under the "circuits" application: NetBox's entire REST API is housed under the API root, `/api/`. The API's URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example, the provider and circuit objects are located under the "circuits" application:
* /api/circuits/providers/ * /api/circuits/providers/
* /api/circuits/circuits/ * /api/circuits/circuits/
@@ -47,9 +13,9 @@ Likewise, the site, rack, and device objects are located under the "DCIM" applic
* /api/dcim/racks/ * /api/dcim/racks/
* /api/dcim/devices/ * /api/dcim/devices/
The full hierarchy of available endpoints can be viewed by navigating to the API root in a web browser. The full hierarchy of available endpoints can be viewed by navigating to the API root (e.g. /api/) in a web browser.
Each model generally has two views associated with it: a list view and a detail view. The list view is used to request a list of multiple objects or to create a new object. The detail view is used to retrieve, update, or delete an existing object. All objects are referenced by their numeric primary key (`id`). Each model generally has two URLs associated with it: a list URL and a detail URL. The list URL is used to request a list of multiple objects or to create a new object. The detail URL is used to retrieve, update, or delete an existing object. All objects are referenced by their numeric primary key (ID).
* /api/dcim/devices/ - List devices or create a new device * /api/dcim/devices/ - List devices or create a new device
* /api/dcim/devices/123/ - Retrieve, update, or delete the device with ID 123 * /api/dcim/devices/123/ - Retrieve, update, or delete the device with ID 123
@@ -88,10 +54,10 @@ The base serializer is used to represent the default view of a model. This inclu
"vid": 101, "vid": 101,
"name": "Users-Floor1", "name": "Users-Floor1",
"tenant": null, "tenant": null,
"status": { "status": [
"value": 1, 1,
"label": "Active" "Active"
}, ],
"role": { "role": {
"id": 9, "id": 9,
"url": "http://localhost:8000/api/ipam/roles/9/", "url": "http://localhost:8000/api/ipam/roles/9/",
@@ -122,37 +88,6 @@ When a base serializer includes one or more nested serializers, the hierarchical
} }
``` ```
## Static Choice Fields
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.
Each choice includes a human-friendly label and its corresponding numeric value. For example, `GET /api/ipam/_choices/prefix:status/` will return:
```
[
{
"value": 0,
"label": "Container"
},
{
"value": 1,
"label": "Active"
},
{
"value": 2,
"label": "Reserved"
},
{
"value": 3,
"label": "Deprecated"
}
]
```
Thus, to set a prefix's status to "Reserved," it would be assigned the integer `2`.
A request for `GET /api/ipam/_choices/` will return choices for _all_ fields belonging to models within the IPAM app.
# Pagination # Pagination
API responses which contain a list of objects (for example, a request to `/api/dcim/devices/`) will be paginated to avoid unnecessary overhead. The root JSON object will contain the following attributes: API responses which contain a list of objects (for example, a request to `/api/dcim/devices/`) will be paginated to avoid unnecessary overhead. The root JSON object will contain the following attributes:

View File

@@ -17,7 +17,7 @@ ADMINS = [
## BANNER_BOTTOM ## BANNER_BOTTOM
Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. HTML is allowed. To replicate the content of the top banner in the bottom banner, set: Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. To replicate the content of the top banner in the bottom banner, set:
``` ```
BANNER_TOP = 'Your banner text' BANNER_TOP = 'Your banner text'
@@ -26,12 +26,6 @@ BANNER_BOTTOM = BANNER_TOP
--- ---
## BANNER_LOGIN
The value of this variable will be displayed on the login page above the login form. HTML is allowed.
---
## BASE_PATH ## BASE_PATH
Default: None Default: None
@@ -141,53 +135,11 @@ An API consumer can request an arbitrary number of objects by appending the "lim
--- ---
## MEDIA_ROOT ## NETBOX_USERNAME
Default: $BASE_DIR/netbox/media/ ## NETBOX_PASSWORD
The file path to the location where media files (such as image attachments) are stored. By default, this is the `netbox/media/` directory within the base NetBox installation path. If provided, NetBox will use these credentials to authenticate against devices when collecting data.
---
## NAPALM_USERNAME
## NAPALM_PASSWORD
NetBox will use these credentials when authenticating to remote devices via the [NAPALM library](https://napalm-automation.net/), if installed. Both parameters are optional.
Note: If SSH public key authentication has been set up for the system account under which NetBox runs, these parameters are not needed.
---
## NAPALM_ARGS
A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](http://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
```
NAPALM_ARGS = {
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
'port': 2222,
}
```
Note: Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
```
NAPALM_USERNAME = 'username'
NAPALM_PASSWORD = 'MySecretPassword'
NAPALM_ARGS = {
'secret': NAPALM_PASSWORD,
# Include any additional args here
}
```
---
## NAPALM_TIMEOUT
Default: 30 seconds
The amount of time (in seconds) to wait for NAPALM to connect to a device.
--- ---
@@ -207,14 +159,6 @@ When determining the primary IP address for a device, IPv6 is preferred over IPv
--- ---
## REPORTS_ROOT
Default: $BASE_DIR/netbox/reports/
The file path to the location where custom reports will be kept. By default, this is the `netbox/reports/` directory within the base NetBox installation path.
---
## TIME_ZONE ## TIME_ZONE
Default: UTC Default: UTC

View File

@@ -96,4 +96,4 @@ VLAN groups can be employed for administrative organization within NetBox. Each
# Services # 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.) A service represents a TCP or UDP service available on a device. 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 a device. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)

View File

@@ -1,29 +0,0 @@
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,53 +0,0 @@
# Utility Views
Utility views are reusable views that handle common CRUD tasks, such as listing and updating objects. Some views operate on individual objects, whereas others (referred to as "bulk" views) operate on multiple objects at once.
## Individual Views
### ObjectListView
Generates a paginated table of objects from a given queryset, which may optionally be filtered.
### ObjectEditView
Updates an object identified by a primary key (PK) or slug. If no existing object is specified, a new object will be created.
### ObjectDeleteView
Deletes an object. The user is redirected to a confirmation page before the deletion is executed.
## Bulk Views
### BulkCreateView
Creates multiple objects at once based on a given pattern. Currently used only for IP addresses.
### BulkImportView
Accepts CSV-formatted data and creates a new object for each line. Creation is all-or-none.
### BulkEditView
Applies changes to multiple objects at once in a two-step operation. First, the list of PKs for selected objects is POSTed and an edit form is presented to the user. On submission of that form, the specified changes are made to all selected objects.
### BulkDeleteView
Deletes multiple objects. The user selects the objects to be deleted and confirms the deletion.
## Component Views
### ComponentCreateView
Create one or more component objects beloning to a parent object (e.g. interfaces attached to a device).
### ComponentEditView
A subclass of `ObjectEditView`: Updates an individual component object.
### ComponentDeleteView
A subclass of `ObjectDeleteView`: Deletes an individual component object.
### BulkComponentCreateView
Create a set of components objects for each of a selected set of parent objects. This view can be used e.g. to create multiple interfaces on multiple devices at once.

View File

@@ -6,7 +6,6 @@ NetBox is an open source web application designed to help manage and document co
* **Equipment racks** - Organized by group and site * **Equipment racks** - Organized by group and site
* **Devices** - Types of devices and where they are installed * **Devices** - Types of devices and where they are installed
* **Connections** - Network, console, and power connections among devices * **Connections** - Network, console, and power connections among devices
* **Virtualization** - Virtual machines and clusters
* **Data circuits** - Long-haul communications circuits and providers * **Data circuits** - Long-haul communications circuits and providers
* **Secrets** - Encrypted storage of sensitive credentials * **Secrets** - Encrypted storage of sensitive credentials
@@ -47,7 +46,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
| HTTP Service | nginx or Apache | | HTTP Service | nginx or Apache |
| WSGI Service | gunicorn or uWSGI | | WSGI Service | gunicorn or uWSGI |
| Application | Django/Python | | Application | Django/Python |
| Database | PostgreSQL 9.4+ | | Database | PostgreSQL |
# Getting Started # Getting Started

View File

@@ -55,7 +55,7 @@ LDAP_IGNORE_CERT_ERRORS = True
## User Authentication ## User Authentication
!!! info !!! info
When using Windows Server 2012, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None. When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
```python ```python
from django_auth_ldap.config import LDAPSearch from django_auth_ldap.config import LDAPSearch
@@ -72,14 +72,11 @@ AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
# You can map user attributes to Django attributes as so. # You can map user attributes to Django attributes as so.
AUTH_LDAP_USER_ATTR_MAP = { AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName", "first_name": "givenName",
"last_name": "sn", "last_name": "sn"
"email": "mail"
} }
``` ```
# User Groups for Permissions # User Groups for Permissions
!!! Info
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
```python ```python
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
@@ -111,3 +108,12 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in. * `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions. * `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions. * `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
It is also possible map user attributes to Django attributes:
```python
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
}
```

View File

@@ -1,33 +0,0 @@
# Migration
Remove Python 2 packages
```no-highlight
# apt-get remove --purge -y python-dev python-pip
```
Install Python 3 packages
```no-highlight
# apt-get install -y python3 python3-dev python3-pip
```
Install Python Packages
```no-highlight
# cd /opt/netbox
# pip3 install -r requirements.txt
```
Gunicorn Update
```no-highlight
# pip uninstall gunicorn
# pip3 install gunicorn
```
Re-install LDAP Module (optional if using LDAP for auth)
```no-highlight
sudo pip3 install django-auth-ldap
```

View File

@@ -1,24 +1,17 @@
# Installation # Installation
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** **Ubuntu**
Python 3: Python 3:
```no-highlight ```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 # apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install3 pip
``` ```
Python 2: Python 2:
```no-highlight ```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 # apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install pip
``` ```
**CentOS** **CentOS**
@@ -27,16 +20,16 @@ Python 3:
```no-highlight ```no-highlight
# yum install -y epel-release # 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 # yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
# easy_install-3.4 pip # easy_install-3.4 pip
# ln -s -f python3.4 /usr/bin/python
``` ```
Python 2: Python 2:
```no-highlight ```no-highlight
# yum install -y epel-release # 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 # yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
# 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. You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
@@ -104,17 +97,6 @@ Python 2:
# pip install -r requirements.txt # 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 `pip -V` or `pip3 -V`.
### NAPALM Automation
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
```
# Configuration # Configuration
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`. Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
@@ -168,13 +150,13 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
# Run Database Migrations # Run Database Migrations
!!! warning !!! 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. The examples on the rest of this page call the `python` executable, which will be Python2 on most systems. Replace this with `python3` if you're running NetBox on Python3.
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): Before NetBox can run, we need to install the database schema. This is done by running `python manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
```no-highlight ```no-highlight
# cd /opt/netbox/netbox/ # cd /opt/netbox/netbox/
# python3 manage.py migrate # python manage.py migrate
Operations to perform: Operations to perform:
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
Running migrations: Running migrations:
@@ -192,7 +174,7 @@ If this step results in a PostgreSQL authentication error, ensure that the usern
NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox: NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox:
```no-highlight ```no-highlight
# python3 manage.py createsuperuser # python manage.py createsuperuser
Username: admin Username: admin
Email address: admin@example.com Email address: admin@example.com
Password: Password:
@@ -203,7 +185,7 @@ Superuser created successfully.
# Collect Static Files # Collect Static Files
```no-highlight ```no-highlight
# python3 manage.py collectstatic --no-input # python manage.py collectstatic --no-input
You have requested to collect static files at the destination You have requested to collect static files at the destination
location as specified in your settings: location as specified in your settings:
@@ -224,7 +206,7 @@ NetBox ships with some initial data to help you get started: RIR definitions, co
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch. This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
```no-highlight ```no-highlight
# python3 manage.py loaddata initial_data # python manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s) Installed 43 object(s) from 4 fixture(s)
``` ```
@@ -233,7 +215,7 @@ Installed 43 object(s) from 4 fixture(s)
At this point, NetBox should be able to run. We can verify this by starting a development instance: At this point, NetBox should be able to run. We can verify this by starting a development instance:
```no-highlight ```no-highlight
# python3 manage.py runserver 0.0.0.0:8000 --insecure # python manage.py runserver 0.0.0.0:8000 --insecure
Performing system checks... Performing system checks...
System check identified no issues (0 silenced). System check identified no issues (0 silenced).

View File

@@ -1,17 +1,12 @@
NetBox requires a PostgreSQL database to store data. This can be hosted locally or on a remote server. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/current/static/datatype-net-types.html).) NetBox requires a PostgreSQL database to store data. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).)
!!! note !!! note
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 7.4. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 6.9. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
!!! warning
NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
# Installation # Installation
**Ubuntu** **Ubuntu**
If a recent enough version of PostgreSQL is not available through your distribution's package manager, you'll need to install it from an official [PostgreSQL repository](https://wiki.postgresql.org/wiki/Apt).
```no-highlight ```no-highlight
# apt-get update # apt-get update
# apt-get install -y postgresql libpq-dev # apt-get install -y postgresql libpq-dev
@@ -19,26 +14,22 @@ If a recent enough version of PostgreSQL is not available through your distribut
**CentOS** **CentOS**
CentOS 7.4 does not ship with a recent enough version of PostgreSQL, so it will need to be installed from an external repository. The instructions below show the installation of PostgreSQL 9.6.
```no-highlight ```no-highlight
# yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm # yum install -y postgresql postgresql-server postgresql-devel
# yum install postgresql96 postgresql96-server postgresql96-devel # postgresql-setup initdb
# /usr/pgsql-9.6/bin/postgresql96-setup initdb
``` ```
CentOS users should modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/9.6/data/pg_hba.conf`. For example: CentOS users should modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/data/pg_hba.conf`. For example:
```no-highlight ```no-highlight
host all all 127.0.0.1/32 md5 host all all 127.0.0.1/32 md5
host all all ::1/128 md5 host all all ::1/128 md5
``` ```
Then, start the service and enable it to run at boot: Then, start the service:
```no-highlight ```no-highlight
# systemctl start postgresql-9.6 # systemctl start postgresql
# systemctl enable postgresql-9.6
``` ```
# Database Creation # Database Creation
@@ -50,7 +41,7 @@ At a minimum, we need to create a database for NetBox and assign it a username a
```no-highlight ```no-highlight
# sudo -u postgres psql # sudo -u postgres psql
psql (9.4.5) psql (9.3.13)
Type "help" for help. Type "help" for help.
postgres=# CREATE DATABASE netbox; postgres=# CREATE DATABASE netbox;
@@ -62,10 +53,10 @@ GRANT
postgres=# \q postgres=# \q
``` ```
You can verify that authentication works issuing the following command and providing the configured password. (Replace `localhost` with your database server if using a remote database.) You can verify that authentication works issuing the following command and providing the configured password:
```no-highlight ```no-highlight
# psql -U netbox -W -h localhost netbox # psql -U netbox -h localhost -W
``` ```
If successful, you will enter a `netbox` prompt. Type `\q` to exit. If successful, you will enter a `postgres` prompt. Type `\q` to exit.

View File

@@ -1,9 +1,13 @@
# Web Server Installation
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence. We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
!!! info !!! info
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed. For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
# Web Server Installation ```no-highlight
# apt-get install -y gunicorn supervisor
```
## Option A: nginx ## Option A: nginx
@@ -51,7 +55,7 @@ Restart the nginx service to use the new configuration.
# service nginx restart # service nginx restart
``` ```
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04). To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04).
## Option B: Apache ## Option B: Apache
@@ -96,16 +100,10 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
# service apache2 restart # service apache2 restart
``` ```
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04). To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-14-04).
# gunicorn Installation # gunicorn Installation
Install gunicorn using `pip3` (Python 3) or `pip` (Python 2):
```no-highlight
# pip3 install gunicorn
```
Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`. Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
```no-highlight ```no-highlight
@@ -118,12 +116,6 @@ user = 'www-data'
# supervisord Installation # supervisord Installation
Install supervisor:
```no-highlight
# apt-get install -y supervisor
```
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`. Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
```no-highlight ```no-highlight

View File

@@ -1,131 +0,0 @@
# NetBox Reports
A NetBox report is a mechanism for validating the integrity of data within NetBox. Running a report allows the user to verify that the objects defined within NetBox meet certain arbitrary conditions. For example, you can write reports to check that:
* All top-of-rack switches have a console connection
* Every router has a loopback interface with an IP address assigned
* Each interface description conforms to a standard format
* Every site has a minimum set of VLANs defined
* All IP addresses have a parent prefix
...and so on. Reports are completely customizable, so there's practically no limit to what you can test for.
## Writing Reports
Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/optional-settings/#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test.
!!! warning
The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file.
For example, we can create a module named `devices.py` to hold all of our reports which pertain to devices in NetBox. Within that module, we might define several reports. Each report is defined as a Python class inheriting from `extras.reports.Report`.
```
from extras.reports import Report
class DeviceConnectionsReport(Report):
description = "Validate the minimum physical connections for each device"
class DeviceIPsReport(Report):
description = "Check that every device has a primary IP address assigned"
```
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, STATUS_ACTIVE
from dcim.models import ConsolePort, Device, PowerPort
from extras.reports import Report
class DeviceConnectionsReport(Report):
description = "Validate the minimum physical connections for each device"
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=STATUS_ACTIVE):
if console_port.cs_port is None:
self.log_failure(
console_port.device,
"No console connection defined for {}".format(console_port.name)
)
elif console_port.connection_status == CONNECTION_STATUS_PLANNED:
self.log_warning(
console_port.device,
"Console connection for {} marked as planned".format(console_port.name)
)
else:
self.log_success(console_port.device)
def test_power_connections(self):
# Check that every active device has at least two connected power supplies.
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:
connected_ports += 1
if power_port.connection_status == CONNECTION_STATUS_PLANNED:
self.log_warning(
device,
"Power connection for {} marked as planned".format(power_port.name)
)
if connected_ports < 2:
self.log_failure(
device,
"{} connected power supplies found (2 needed)".format(connected_ports)
)
else:
self.log_success(device)
```
As you can see, reports are completely customizable. Validation logic can be as simple or as complex as needed.
!!! warning
Reports should never alter data: If you find yourself using the `create()`, `save()`, `update()`, or `delete()` methods on objects within reports, stop and re-evaluate what you're trying to accomplish. Note that there are no safeguards against the accidental alteration or destruction of data.
The following methods are available to log results within a report:
* log(message)
* log_success(object, message=None)
* log_info(object, message)
* log_warning(object, message)
* log_failure(object, message)
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status.
To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`.
Once you have created a report, it will appear in the reports list. Initially, reports will have no results associated with them. To generate results, run the report.
## Running Reports
### Via the Web UI
Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Note that a user must have permission to create ReportResults in order to run reports. (Permissions can be assigned through the admin UI.)
Once a report has been run, its associated results will be included in the report view.
### Via the API
To run a report via the API, simply issue a POST request to its `run` endpoint. Reports are identified by their module and class name.
```
POST /api/extras/reports/<module>.<name>/run/
```
Our example report above would be called as:
```
POST /api/extras/reports/devices.DeviceConnectionsReport/run/
```
### Via the CLI
Reports can be run on the CLI by invoking the management command:
```
python3 manage.py runreport <module>
```
One or more report modules may be specified.

View File

@@ -1,194 +0,0 @@
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
```
This will launch a customized version of [the built-in Django shell](https://docs.djangoproject.com/en/dev/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
```
$ ./manage.py nbshell
### NetBox interactive shell (jstretch-laptop)
### Python 2.7.6 | Django 1.11.3 | NetBox 2.1.0-dev
### lsmodels() will show available models. Use help(<model>) for more info.
```
The function `lsmodels()` will print a list of all available NetBox models:
```
>>> lsmodels()
DCIM:
ConsolePort
ConsolePortTemplate
ConsoleServerPort
ConsoleServerPortTemplate
Device
...
```
## Querying Objects
Objects are retrieved by forming a [Django queryset](https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-objects). The base queryset for an object takes the form `<model>.objects.all()`, which will return a (truncated) list of all objects of that type.
```
>>> Device.objects.all()
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice4>, <Device: TestDevice5>, '...(remaining elements truncated)...']>
```
Use a `for` loop to cycle through all objects in the list:
```
>>> for device in Device.objects.all():
... print(device.name, device.device_type)
...
(u'TestDevice1', <DeviceType: PacketThingy 9000>)
(u'TestDevice2', <DeviceType: PacketThingy 9000>)
(u'TestDevice3', <DeviceType: PacketThingy 9000>)
(u'TestDevice4', <DeviceType: PacketThingy 9000>)
(u'TestDevice5', <DeviceType: PacketThingy 9000>)
...
```
To count all objects matching the query, replace `all()` with `count()`:
```
>>> Device.objects.count()
1274
```
To retrieve a particular object (typically by its primary key or other unique field), use `get()`:
```
>>> Site.objects.get(pk=7)
<Site: Test Lab>
```
### Filtering Querysets
In most cases, you want to retrieve only a specific subset of objects. To filter a queryset, replace `all()` with `filter()` and pass one or more keyword arguments. For example:
```
>>> Device.objects.filter(status=STATUS_ACTIVE)
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice8>, <Device: TestDevice9>, '...(remaining elements truncated)...']>
```
Querysets support slicing to return a specific range of objects.
```
>>> Device.objects.filter(status=STATUS_ACTIVE)[:3]
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>]>
```
The `count()` method can be appended to the queryset to return a count of objects rather than the full list.
```
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
982
```
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')
```
This approach can span multiple levels of relations. For example, the following will return all IP addresses assigned to a device in North America:
```
>>> IPAddress.objects.filter(interface__device__site__region__slug='north-america')
```
!!! note
While the above query is functional, it is very inefficient. There are ways to optimize such requests, however they are out of the scope of this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/dev/ref/models/querysets/) documentation.
Reverse relationships can be traversed as well. For example, the following will find all devices with an interface named "em0":
```
>>> Device.objects.filter(interfaces__name='em0')
```
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive).
```
>>> Device.objects.filter(name__icontains='testdevice')
```
Similarly, numeric fields can be filtered by values less than, greater than, and/or equal to a given value.
```
>>> VLAN.objects.filter(vid__gt=2000)
```
Multiple filters can be combined to further refine a queryset.
```
>>> VLAN.objects.filter(vid__gt=2000, name__icontains='engineering')
```
To return the inverse of a filtered queryset, use `exclude()` instead of `filter()`.
```
>>> Device.objects.count()
4479
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
4133
>>> Device.objects.exclude(status=STATUS_ACTIVE).count()
346
```
!!! info
The examples above are intended only to provide a cursory introduction to queryset filtering. For an exhaustive list of the available filters, please consult the [Django queryset API docs](https://docs.djangoproject.com/en/dev/ref/models/querysets/).
## Creating and Updating Objects
New objects can be created by instantiating the desired model, defining values for all required attributes, and calling `save()` on the instance.
```
>>> lab1 = Site.objects.get(pk=7)
>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1)
>>> myvlan.save()
```
Alternatively, the above can be performed as a single operation:
```
>>> VLAN(vid=123, name='MyNewVLAN', site=Site.objects.get(pk=7)).save()
```
To modify an object, retrieve it, update the desired field(s), and call `save()` again.
```
>>> vlan = VLAN.objects.get(pk=1280)
>>> vlan.name
u'MyNewVLAN'
>>> vlan.name = 'BetterName'
>>> vlan.save()
>>> VLAN.objects.get(pk=1280).name
u'BetterName'
```
!!! warning
The Django ORM provides methods to create/edit many objects at once, namely `bulk_create()` and `update()`. These are best avoided in most cases as they bypass a model's built-in validation and can easily lead to database corruption if not used carefully.
## Deleting Objects
To delete an object, simply call `delete()` on its instance. This will return a dictionary of all objects (including related objects) which have been deleted as a result of this operation.
```
>>> vlan
<VLAN: 123 (BetterName)>
>>> vlan.delete()
(1, {u'extras.CustomFieldValue': 0, u'ipam.VLAN': 1})
```
To delete multiple objects at once, call `delete()` on a filtered queryset. It's a good idea to always sanity-check the count of selected objects _before_ deleting them.
```
>>> Device.objects.filter(name__icontains='test').count()
27
>>> Device.objects.filter(name__icontains='test').delete()
(35, {u'extras.CustomFieldValue': 0, u'dcim.DeviceBay': 0, u'secrets.Secret': 0, u'dcim.InterfaceConnection': 4, u'extras.ImageAttachment': 0, u'dcim.Device': 27, u'dcim.Interface': 4, u'dcim.ConsolePort': 0, u'dcim.PowerPort': 0})
```
!!! warning
Deletions are immediate and irreversible. Always think very carefully before calling `delete()` on an instance or queryset.

View File

@@ -8,7 +8,6 @@ pages:
- 'Web Server': 'installation/web-server.md' - 'Web Server': 'installation/web-server.md'
- 'LDAP (Optional)': 'installation/ldap.md' - 'LDAP (Optional)': 'installation/ldap.md'
- 'Upgrading': 'installation/upgrading.md' - 'Upgrading': 'installation/upgrading.md'
- 'Migrating to Python3': 'installation/migrating-to-python3.md'
- 'Configuration': - 'Configuration':
- 'Mandatory Settings': 'configuration/mandatory-settings.md' - 'Mandatory Settings': 'configuration/mandatory-settings.md'
- 'Optional Settings': 'configuration/optional-settings.md' - 'Optional Settings': 'configuration/optional-settings.md'
@@ -18,18 +17,12 @@ pages:
- 'IPAM': 'data-model/ipam.md' - 'IPAM': 'data-model/ipam.md'
- 'Secrets': 'data-model/secrets.md' - 'Secrets': 'data-model/secrets.md'
- 'Tenancy': 'data-model/tenancy.md' - 'Tenancy': 'data-model/tenancy.md'
- 'Virtualization': 'data-model/virtualization.md'
- 'Extras': 'data-model/extras.md' - 'Extras': 'data-model/extras.md'
- 'API': - 'API':
- 'Overview': 'api/overview.md' - 'Overview': 'api/overview.md'
- 'Authentication': 'api/authentication.md' - 'Authentication': 'api/authentication.md'
- 'Working with Secrets': 'api/working-with-secrets.md' - 'Working with Secrets': 'api/working-with-secrets.md'
- 'Examples': 'api/examples.md' - 'Examples': 'api/examples.md'
- 'Miscellaneous':
- 'Reports': 'miscellaneous/reports.md'
- 'Shell': 'miscellaneous/shell.md'
- 'Development':
- 'Utility Views': 'development/utility-views.md'
markdown_extensions: markdown_extensions:
- admonition: - admonition:

View File

@@ -6,7 +6,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ValidatedModelSerializer from utilities.api import ModelValidationMixin
# #
@@ -45,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
# Circuit types # Circuit types
# #
class CircuitTypeSerializer(ValidatedModelSerializer): class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = CircuitType model = CircuitType
@@ -111,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
] ]
class WritableCircuitTerminationSerializer(ValidatedModelSerializer): class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination

View File

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

View File

@@ -1,29 +1,20 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.shortcuts import get_object_or_404
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from django.shortcuts import get_object_or_404
from circuits import filters from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_PROVIDER from utilities.api import WritableSerializerMixin
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers from . import serializers
#
# Field choices
#
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(CircuitTermination, ['term_side']),
)
# #
# Providers # Providers
# #

View File

@@ -1,12 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.models import Site from dcim.models import Site
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NumericInFilter from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Provider, Circuit, CircuitTermination, CircuitType from .models import Provider, Circuit, CircuitTermination, CircuitType
@@ -77,12 +78,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Circuit type (slug)', label='Circuit type (slug)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = django_filters.ModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant__slug', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',

View File

@@ -11,6 +11,7 @@ from utilities.forms import (
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField, APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
SmallTextarea, SlugField, SmallTextarea, SlugField,
) )
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -84,17 +85,6 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
fields = ['name', 'slug'] fields = ['name', 'slug']
class CircuitTypeCSVForm(forms.ModelForm):
slug = SlugField()
class Meta:
model = CircuitType
fields = ['name', 'slug']
help_texts = {
'name': 'Name of circuit type',
}
# #
# Circuits # Circuits
# #
@@ -180,7 +170,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')), queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
to_field_name='slug' to_field_name='slug'
) )
commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)')
# #
@@ -255,7 +244,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
# Initialize helper selectors # Initialize helper selectors
instance = kwargs.get('instance') instance = kwargs.get('instance')
if instance and instance.interface is not None: if instance and instance.interface is not None:
initial = kwargs.get('initial', {}).copy() initial = kwargs.get('initial', {})
initial['rack'] = instance.interface.device.rack initial['rack'] = instance.interface.device.rack
initial['device'] = instance.interface.device initial['device'] = instance.interface.device
kwargs['initial'] = initial kwargs['initial'] = initial

View File

@@ -8,11 +8,27 @@ from django.utils.encoding import python_2_unicode_compatible
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format from utilities.utils import csv_format
from utilities.models import CreatedUpdatedModel
from .constants import * from .constants import *
def humanize_speed(speed):
"""
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
"""
if speed >= 1000000000 and speed % 1000000000 == 0:
return '{} Tbps'.format(speed / 1000000000)
elif speed >= 1000000 and speed % 1000000 == 0:
return '{} Gbps'.format(speed / 1000000)
elif speed >= 1000 and speed % 1000 == 0:
return '{} Mbps'.format(speed / 1000)
elif speed >= 1000:
return '{} Mbps'.format(float(speed) / 1000)
else:
return '{} Kbps'.format(speed)
@python_2_unicode_compatible @python_2_unicode_compatible
class Provider(CreatedUpdatedModel, CustomFieldModel): class Provider(CreatedUpdatedModel, CustomFieldModel):
""" """
@@ -123,6 +139,10 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
def termination_z(self): def termination_z(self):
return self._get_termination('Z') return self._get_termination('Z')
def commit_rate_human(self):
return '' if not self.commit_rate else humanize_speed(self.commit_rate)
commit_rate_human.admin_order_field = 'commit_rate'
@python_2_unicode_compatible @python_2_unicode_compatible
class CircuitTermination(models.Model): class CircuitTermination(models.Model):
@@ -153,3 +173,11 @@ class CircuitTermination(models.Model):
return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side) return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
except CircuitTermination.DoesNotExist: except CircuitTermination.DoesNotExist:
return None return None
def port_speed_human(self):
return humanize_speed(self.port_speed)
port_speed_human.admin_order_field = 'port_speed'
def upstream_speed_human(self):
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
upstream_speed_human.admin_order_field = 'upstream_speed'

View File

@@ -1,12 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django.utils.safestring import mark_safe
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_ACTIONS = """ CIRCUITTYPE_ACTIONS = """
{% if perms.circuit.change_circuittype %} {% 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> <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>
@@ -14,21 +14,6 @@ CIRCUITTYPE_ACTIONS = """
""" """
class CircuitTerminationColumn(tables.Column):
def render(self, value):
if value.interface:
return mark_safe('<a href="{}" title="{}">{}</a>'.format(
value.interface.device.get_absolute_url(),
value.site,
value.interface.device
))
return mark_safe('<a href="{}">{}</a>'.format(
value.site.get_absolute_url(),
value.site
))
# #
# Providers # Providers
# #
@@ -36,20 +21,21 @@ class CircuitTerminationColumn(tables.Column):
class ProviderTable(BaseTable): class ProviderTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Provider
fields = ('pk', 'name', 'asn', 'account',)
class ProviderDetailTable(ProviderTable):
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(ProviderTable.Meta):
model = Provider model = Provider
fields = ('pk', 'name', 'asn', 'account', 'circuit_count') fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
class ProviderSearchTable(SearchTable):
name = tables.LinkColumn()
class Meta(SearchTable.Meta):
model = Provider
fields = ('name', 'asn', 'account')
# #
# Circuit types # Circuit types
# #
@@ -76,9 +62,31 @@ class CircuitTable(BaseTable):
cid = tables.LinkColumn(verbose_name='ID') cid = tables.LinkColumn(verbose_name='ID')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side') a_side = tables.LinkColumn(
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side') 'dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
args=[Accessor('termination_a.site.slug')]
)
z_side = tables.LinkColumn(
'dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
args=[Accessor('termination_z.site.slug')]
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Circuit model = Circuit
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'termination_a', 'termination_z', 'description') fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
class CircuitSearchTable(SearchTable):
cid = tables.LinkColumn(verbose_name='ID')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
a_side = tables.LinkColumn(
'dcim:site', accessor=Accessor('termination_a.site'), args=[Accessor('termination_a.site.slug')]
)
z_side = tables.LinkColumn(
'dcim:site', accessor=Accessor('termination_z.site'), args=[Accessor('termination_z.site.slug')]
)
class Meta(SearchTable.Meta):
model = Circuit
fields = ('cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')

View File

@@ -1,15 +1,14 @@
from __future__ import unicode_literals 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 import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z from django.contrib.auth.models import User
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from django.urls import reverse
from dcim.models import Site from dcim.models import Site
from extras.constants import GRAPH_TYPE_PROVIDER from extras.models import Graph, GRAPH_TYPE_PROVIDER
from extras.models import Graph from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@@ -4,6 +4,7 @@ from django.conf.urls import url
from . import views from . import views
app_name = 'circuits' app_name = 'circuits'
urlpatterns = [ urlpatterns = [
@@ -20,7 +21,6 @@ urlpatterns = [
# Circuit types # Circuit types
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'), url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
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/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-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),

View File

@@ -15,8 +15,7 @@ from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .constants import TERM_SIDE_A, TERM_SIDE_Z from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
from .models import Circuit, CircuitTermination, CircuitType, Provider
# #
@@ -27,7 +26,7 @@ class ProviderListView(ObjectListView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits')) queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filter = filters.ProviderFilter filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm filter_form = forms.ProviderFilterForm
table = tables.ProviderDetailTable table = tables.ProviderTable
template_name = 'circuits/provider_list.html' template_name = 'circuits/provider_list.html'
@@ -53,7 +52,7 @@ class ProviderView(View):
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView): class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_provider' permission_required = 'circuits.add_provider'
model = Provider model = Provider
model_form = forms.ProviderForm form_class = forms.ProviderForm
template_name = 'circuits/provider_edit.html' template_name = 'circuits/provider_edit.html'
default_return_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
@@ -79,8 +78,8 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_provider' permission_required = 'circuits.change_provider'
cls = Provider cls = Provider
filter = filters.ProviderFilter filter = filters.ProviderFilter
table = tables.ProviderTable
form = forms.ProviderBulkEditForm form = forms.ProviderBulkEditForm
template_name = 'circuits/provider_bulk_edit.html'
default_return_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
@@ -88,7 +87,6 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider' permission_required = 'circuits.delete_provider'
cls = Provider cls = Provider
filter = filters.ProviderFilter filter = filters.ProviderFilter
table = tables.ProviderTable
default_return_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
@@ -105,7 +103,7 @@ class CircuitTypeListView(ObjectListView):
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView): class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittype' permission_required = 'circuits.add_circuittype'
model = CircuitType model = CircuitType
model_form = forms.CircuitTypeForm form_class = forms.CircuitTypeForm
def get_return_url(self, request, obj): def get_return_url(self, request, obj):
return reverse('circuits:circuittype_list') return reverse('circuits:circuittype_list')
@@ -115,18 +113,9 @@ class CircuitTypeEditView(CircuitTypeCreateView):
permission_required = 'circuits.change_circuittype' permission_required = 'circuits.change_circuittype'
class CircuitTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'circuits.add_circuittype'
model_form = forms.CircuitTypeCSVForm
table = tables.CircuitTypeTable
default_return_url = 'circuits:circuittype_list'
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype' permission_required = 'circuits.delete_circuittype'
cls = CircuitType cls = CircuitType
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable
default_return_url = 'circuits:circuittype_list' default_return_url = 'circuits:circuittype_list'
@@ -135,11 +124,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class CircuitListView(ObjectListView): class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related( queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
'provider', 'type', 'tenant'
).prefetch_related(
'terminations__site', 'terminations__interface__device'
)
filter = filters.CircuitFilter filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm filter_form = forms.CircuitFilterForm
table = tables.CircuitTable table = tables.CircuitTable
@@ -172,7 +157,7 @@ class CircuitView(View):
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView): class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuit' permission_required = 'circuits.add_circuit'
model = Circuit model = Circuit
model_form = forms.CircuitForm form_class = forms.CircuitForm
template_name = 'circuits/circuit_edit.html' template_name = 'circuits/circuit_edit.html'
default_return_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
@@ -197,19 +182,16 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_circuit' permission_required = 'circuits.change_circuit'
cls = Circuit cls = Circuit
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter filter = filters.CircuitFilter
table = tables.CircuitTable
form = forms.CircuitBulkEditForm form = forms.CircuitBulkEditForm
template_name = 'circuits/circuit_bulk_edit.html'
default_return_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit' permission_required = 'circuits.delete_circuit'
cls = Circuit cls = Circuit
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter filter = filters.CircuitFilter
table = tables.CircuitTable
default_return_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
@@ -265,7 +247,7 @@ def circuit_terminations_swap(request, pk):
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView): class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittermination' permission_required = 'circuits.add_circuittermination'
model = CircuitTermination model = CircuitTermination
model_form = forms.CircuitTerminationForm form_class = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html' template_name = 'circuits/circuittermination_edit.html'
def alter_obj(self, obj, request, url_args, url_kwargs): def alter_obj(self, obj, request, url_args, url_kwargs):

View File

@@ -1,26 +1,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from ipam.models import IPAddress
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
from dcim.constants import (
CONNECTION_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
)
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
RackReservation, RackRole, Region, Site, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
) )
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import IPAddress
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
from virtualization.models import Cluster
# #
@@ -43,7 +38,7 @@ class RegionSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'parent'] fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(ValidatedModelSerializer): class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Region model = Region
@@ -105,7 +100,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name', 'slug'] fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(ValidatedModelSerializer): class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RackGroup model = RackGroup
@@ -116,7 +111,7 @@ class WritableRackGroupSerializer(ValidatedModelSerializer):
# Rack roles # Rack roles
# #
class RackRoleSerializer(ValidatedModelSerializer): class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RackRole model = RackRole
@@ -146,8 +141,8 @@ class RackSerializer(CustomFieldModelSerializer):
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width', 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height',
'u_height', 'desc_units', 'comments', 'custom_fields', 'desc_units', 'comments', 'custom_fields',
] ]
@@ -164,8 +159,8 @@ class WritableRackSerializer(CustomFieldModelSerializer):
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width', 'u_height', 'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
'desc_units', 'comments', 'custom_fields', 'comments', 'custom_fields',
] ]
# Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This # Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
# prevents facility_id from being interpreted as a required field. # prevents facility_id from being interpreted as a required field.
@@ -221,18 +216,18 @@ class RackReservationSerializer(serializers.ModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'description'] fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(ValidatedModelSerializer): class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['id', 'rack', 'units', 'user', 'description'] fields = ['id', 'rack', 'units', 'description']
# #
# Manufacturers # Manufacturers
# #
class ManufacturerSerializer(ValidatedModelSerializer): class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
@@ -297,7 +292,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer): class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
@@ -316,7 +311,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
@@ -335,7 +330,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer): class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
@@ -354,7 +349,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer): class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
@@ -374,7 +369,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only'] fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class WritableInterfaceTemplateSerializer(ValidatedModelSerializer): class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
@@ -393,7 +388,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer): class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
@@ -404,11 +399,11 @@ class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
# Device roles # Device roles
# #
class DeviceRoleSerializer(ValidatedModelSerializer): class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role'] fields = ['id', 'name', 'slug', 'color']
class NestedDeviceRoleSerializer(serializers.ModelSerializer): class NestedDeviceRoleSerializer(serializers.ModelSerializer):
@@ -423,11 +418,11 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
# Platforms # Platforms
# #
class PlatformSerializer(ValidatedModelSerializer): class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Platform model = Platform
fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client'] fields = ['id', 'name', 'slug', 'rpc_client']
class NestedPlatformSerializer(serializers.ModelSerializer): class NestedPlatformSerializer(serializers.ModelSerializer):
@@ -451,15 +446,6 @@ class DeviceIPAddressSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'family', 'address'] fields = ['id', 'url', 'family', 'address']
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
class NestedClusterSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
class Meta:
model = Cluster
fields = ['id', 'url', 'name']
class DeviceSerializer(CustomFieldModelSerializer): class DeviceSerializer(CustomFieldModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer() device_role = NestedDeviceRoleSerializer()
@@ -473,14 +459,13 @@ class DeviceSerializer(CustomFieldModelSerializer):
primary_ip4 = DeviceIPAddressSerializer() primary_ip4 = DeviceIPAddressSerializer()
primary_ip6 = DeviceIPAddressSerializer() primary_ip6 = DeviceIPAddressSerializer()
parent_device = serializers.SerializerMethodField() parent_device = serializers.SerializerMethodField()
cluster = NestedClusterSerializer()
class Meta: class Meta:
model = Device model = Device
fields = [ fields = [
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
'cluster', 'comments', 'custom_fields', 'comments', 'custom_fields',
] ]
def get_parent_device(self, obj): def get_parent_device(self, obj):
@@ -488,10 +473,14 @@ class DeviceSerializer(CustomFieldModelSerializer):
device_bay = obj.parent_bay device_bay = obj.parent_bay
except DeviceBay.DoesNotExist: except DeviceBay.DoesNotExist:
return None return None
context = {'request': self.context['request']} return {
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data 'id': device_bay.device.pk,
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data 'name': device_bay.device.name,
return data 'device_bay': {
'id': device_bay.pk,
'name': device_bay.name,
}
}
class WritableDeviceSerializer(CustomFieldModelSerializer): class WritableDeviceSerializer(CustomFieldModelSerializer):
@@ -500,7 +489,7 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
model = Device model = Device
fields = [ fields = [
'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack', 'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack',
'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'cluster', 'comments', 'custom_fields', 'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'comments', 'custom_fields',
] ]
validators = [] validators = []
@@ -531,7 +520,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_console'] read_only_fields = ['connected_console']
class WritableConsoleServerPortSerializer(ValidatedModelSerializer): class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
@@ -551,7 +540,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
class WritableConsolePortSerializer(ValidatedModelSerializer): class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsolePort model = ConsolePort
@@ -571,7 +560,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_port'] read_only_fields = ['connected_port']
class WritablePowerOutletSerializer(ValidatedModelSerializer): class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
@@ -591,7 +580,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
class WritablePowerPortSerializer(ValidatedModelSerializer): class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerPort model = PowerPort
@@ -679,7 +668,7 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
] ]
class WritableInterfaceSerializer(ValidatedModelSerializer): class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Interface model = Interface
@@ -701,15 +690,7 @@ class DeviceBaySerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'installed_device'] fields = ['id', 'device', 'name', 'installed_device']
class NestedDeviceBaySerializer(serializers.ModelSerializer): class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
class Meta:
model = DeviceBay
fields = ['id', 'url', 'name']
class WritableDeviceBaySerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceBay model = DeviceBay
@@ -732,7 +713,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
] ]
class WritableInventoryItemSerializer(ValidatedModelSerializer): class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
@@ -764,7 +745,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'connection_status'] fields = ['id', 'url', 'connection_status']
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer): class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = InterfaceConnection model = InterfaceConnection

View File

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

View File

@@ -1,46 +1,27 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from django.conf import settings
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
from dcim import filters from django.conf import settings
from django.shortcuts import get_object_or_404
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, RackReservation, RackRole, Region, Site,
) )
from dcim import filters
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.api import ( from utilities.api import ServiceUnavailable, WritableSerializerMixin
IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin,
)
from . import serializers
from .exceptions import MissingFilterException from .exceptions import MissingFilterException
from . import serializers
#
# Field choices
#
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Device, ['face', 'status']),
(ConsolePort, ['connection_status']),
(Interface, ['form_factor']),
(InterfaceConnection, ['connection_status']),
(InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']),
(Rack, ['type', 'width']),
)
# #
@@ -243,73 +224,27 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
write_serializer_class = serializers.WritableDeviceSerializer write_serializer_class = serializers.WritableDeviceSerializer
filter_class = filters.DeviceFilter filter_class = filters.DeviceFilter
@detail_route(url_path='napalm') @detail_route(url_path='lldp-neighbors')
def napalm(self, request, pk): def lldp_neighbors(self, request, pk):
""" """
Execute a NAPALM method on a Device Retrieve live LLDP neighbors of a device
""" """
device = get_object_or_404(Device, pk=pk) device = get_object_or_404(Device, pk=pk)
if not device.primary_ip: if not device.primary_ip:
raise ServiceUnavailable("This device does not have a primary IP address configured.") raise ServiceUnavailable("No IP configured for this device.")
if device.platform is None:
raise ServiceUnavailable("No platform is configured for this device.")
if not device.platform.napalm_driver:
raise ServiceUnavailable("No NAPALM driver is configured for this device's platform ().".format(
device.platform
))
# Check that NAPALM is installed RPC = device.get_rpc_client()
if not RPC:
raise ServiceUnavailable("No RPC client available for this platform ({}).".format(device.platform))
# Connect to device and retrieve inventory info
try: try:
import napalm with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client:
except ImportError: lldp_neighbors = rpc_client.get_lldp_neighbors()
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.") except:
raise ServiceUnavailable("Error connecting to the remote device.")
# TODO: Remove support for NAPALM < 2.0 return Response(lldp_neighbors)
try:
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
except ImportError:
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
# Validate the configured driver
try:
driver = napalm.get_network_driver(device.platform.napalm_driver)
except ModuleImportError:
raise ServiceUnavailable("NAPALM driver for platform {} not found: {}.".format(
device.platform, device.platform.napalm_driver
))
# Verify user permission
if not request.user.has_perm('dcim.napalm_read'):
return HttpResponseForbidden()
# 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)
d = driver(
hostname=ip_address,
username=settings.NAPALM_USERNAME,
password=settings.NAPALM_PASSWORD,
timeout=settings.NAPALM_TIMEOUT,
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))
d.close()
return Response(response)
# #
@@ -411,7 +346,7 @@ class ConnectedDeviceViewSet(ViewSet):
* `peer-device`: The name of the peer device * `peer-device`: The name of the peer device
* `peer-interface`: The name of the peer interface * `peer-interface`: The name of the peer interface
""" """
permission_classes = [IsAuthenticatedOrLoginNotRequired] permission_classes = [IsAuthenticated]
def get_view_name(self): def get_view_name(self):
return "Connected Device Locator" return "Connected Device Locator"

View File

@@ -58,7 +58,6 @@ IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050 IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100 IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150 IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_CX4 = 1170
IFACE_FF_10GE_SFP_PLUS = 1200 IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300 IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310 IFACE_FF_10GE_XENPAK = 1310
@@ -66,9 +65,6 @@ IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350 IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400 IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_100GE_CFP = 1500 IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_CFP2 = 1510
IFACE_FF_100GE_CFP4 = 1520
IFACE_FF_100GE_CPAK = 1550
IFACE_FF_100GE_QSFP28 = 1600 IFACE_FF_100GE_QSFP28 = 1600
# Wireless # Wireless
IFACE_FF_80211A = 2600 IFACE_FF_80211A = 2600
@@ -102,7 +98,7 @@ IFACE_FF_CHOICES = [
[ [
[IFACE_FF_VIRTUAL, 'Virtual'], [IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'], [IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
], ]
], ],
[ [
'Ethernet (fixed)', 'Ethernet (fixed)',
@@ -110,7 +106,6 @@ IFACE_FF_CHOICES = [
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'], [IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'], [IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'], [IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'],
] ]
], ],
[ [
@@ -125,9 +120,6 @@ IFACE_FF_CHOICES = [
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'], [IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'], [IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'], [IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
[IFACE_FF_100GE_CFP4, 'CFP4 (100GE)'],
[IFACE_FF_100GE_CPAK, 'Cisco CPAK (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'], [IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
] ]
], ],

View File

@@ -1,33 +1,29 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from netaddr.core import AddrFormatError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from netaddr import EUI
from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableCharFieldFilter, NumericInFilter from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from virtualization.models import Cluster
from .constants import (
IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, STATUS_CHOICES, VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
)
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
RackReservation, RackRole, Region, Site, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
) )
class RegionFilter(django_filters.FilterSet): class RegionFilter(django_filters.FilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = NullableModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Parent region (ID)', label='Parent region (ID)',
) )
parent = django_filters.ModelMultipleChoiceFilter( parent = NullableModelMultipleChoiceFilter(
name='parent__slug',
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Parent region (slug)', label='Parent region (slug)',
@@ -44,22 +40,20 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
region_id = django_filters.ModelMultipleChoiceFilter( region_id = NullableModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Region (ID)', label='Region (ID)',
) )
region = django_filters.ModelMultipleChoiceFilter( region = NullableModelMultipleChoiceFilter(
name='region__slug',
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = django_filters.ModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant__slug',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
@@ -119,7 +113,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
facility_id = NullableCharFieldFilter()
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@@ -130,32 +123,32 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = django_filters.ModelMultipleChoiceFilter( group_id = NullableModelMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
group = django_filters.ModelMultipleChoiceFilter( group = NullableModelMultipleChoiceFilter(
name='group__slug', name='group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = django_filters.ModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant__slug', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
role_id = django_filters.ModelMultipleChoiceFilter( role_id = NullableModelMultipleChoiceFilter(
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
role = django_filters.ModelMultipleChoiceFilter( role = NullableModelMultipleChoiceFilter(
name='role__slug', name='role',
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
@@ -163,7 +156,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Rack model = Rack
fields = ['serial', 'type', 'width', 'u_height', 'desc_units'] fields = ['facility_id', 'type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -171,7 +164,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter( return queryset.filter(
Q(name__icontains=value) | Q(name__icontains=value) |
Q(facility_id__icontains=value) | Q(facility_id__icontains=value) |
Q(serial__icontains=value.strip()) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )
@@ -197,13 +189,13 @@ class RackReservationFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = django_filters.ModelMultipleChoiceFilter( group_id = NullableModelMultipleChoiceFilter(
name='rack__group', name='rack__group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
group = django_filters.ModelMultipleChoiceFilter( group = NullableModelMultipleChoiceFilter(
name='rack__group__slug', name='rack__group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
@@ -279,7 +271,6 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceTypeComponentFilterSet(django_filters.FilterSet): class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter( devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
name='device_type_id',
label='Device type (ID)', label='Device type (ID)',
) )
@@ -372,28 +363,26 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = django_filters.ModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant__slug', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
platform_id = django_filters.ModelMultipleChoiceFilter( platform_id = NullableModelMultipleChoiceFilter(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
label='Platform (ID)', label='Platform (ID)',
) )
platform = django_filters.ModelMultipleChoiceFilter( platform = NullableModelMultipleChoiceFilter(
name='platform__slug', name='platform',
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Platform (slug)', label='Platform (slug)',
) )
name = NullableCharFieldFilter()
asset_tag = NullableCharFieldFilter()
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@@ -409,15 +398,11 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
label='Rack group (ID)', label='Rack group (ID)',
) )
rack_id = django_filters.ModelMultipleChoiceFilter( rack_id = NullableModelMultipleChoiceFilter(
name='rack', name='rack',
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label='Rack (ID)', label='Rack (ID)',
) )
cluster_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cluster.objects.all(),
label='VM cluster (ID)',
)
model = django_filters.ModelMultipleChoiceFilter( model = django_filters.ModelMultipleChoiceFilter(
name='device_type__slug', name='device_type__slug',
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
@@ -425,8 +410,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Device model (slug)', label='Device model (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES, choices=STATUS_CHOICES
null_value=None
) )
is_full_depth = django_filters.BooleanFilter( is_full_depth = django_filters.BooleanFilter(
name='device_type__is_full_depth', name='device_type__is_full_depth',
@@ -455,7 +439,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Device model = Device
fields = ['serial'] fields = ['name', 'serial', 'asset_tag']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -473,8 +457,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value: if not value:
return queryset return queryset
try: try:
mac = EUI(value.strip()) return queryset.filter(interfaces__mac_address=value).distinct()
return queryset.filter(interfaces__mac_address=mac).distinct()
except AddrFormatError: except AddrFormatError:
return queryset.none() return queryset.none()
@@ -586,8 +569,7 @@ class InterfaceFilter(django_filters.FilterSet):
if not value: if not value:
return queryset return queryset
try: try:
mac = EUI(value.strip()) return queryset.filter(mac_address=value)
return queryset.filter(mac_address=mac)
except AddrFormatError: except AddrFormatError:
return queryset.none() return queryset.none()
@@ -600,7 +582,7 @@ class DeviceBayFilter(DeviceComponentFilterSet):
class InventoryItemFilter(DeviceComponentFilterSet): class InventoryItemFilter(DeviceComponentFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = NullableModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(), queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)', label='Parent inventory item (ID)',
) )
@@ -614,11 +596,10 @@ class InventoryItemFilter(DeviceComponentFilterSet):
to_field_name='slug', to_field_name='slug',
label='Manufacturer (slug)', label='Manufacturer (slug)',
) )
asset_tag = NullableCharFieldFilter()
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['name', 'part_id', 'serial', 'discovered'] fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
class ConsoleConnectionFilter(django_filters.FilterSet): class ConsoleConnectionFilter(django_filters.FilterSet):

View File

@@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from netaddr import EUI, AddrFormatError
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from netaddr import EUI, AddrFormatError
# #

View File

@@ -1,12 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from mptt.forms import TreeNodeChoiceField
import re import re
from django import forms from django import forms
from django.contrib.auth.models import User
from django.contrib.postgres.forms.array import SimpleArrayField from django.contrib.postgres.forms.array import SimpleArrayField
from django.db.models import Count, Q from django.db.models import Count, Q
from mptt.forms import TreeNodeChoiceField
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress from ipam.models import IPAddress
@@ -14,24 +13,27 @@ from tenancy.forms import TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
SlugField, FilterTreeNodeMultipleChoiceField, FilterTreeNodeMultipleChoiceField,
)
from virtualization.models import Cluster
from .constants import (
CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES,
RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, STATUS_CHOICES,
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
) )
from .formfields import MACAddressFormField from .formfields import MACAddressFormField
from .models import ( from .models import (
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface,
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
RackRole, Region, Site, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
) )
FORM_STATUS_CHOICES = [
['', '---------'],
]
FORM_STATUS_CHOICES += STATUS_CHOICES
DEVICE_BY_PK_RE = '{\d+\}' DEVICE_BY_PK_RE = '{\d+\}'
@@ -47,6 +49,15 @@ def get_device_by_name_or_pk(name):
return device return device
class DeviceComponentForm(BootstrapMixin, forms.Form):
"""
Allow inclusion of the parent device as context for limiting field choices.
"""
def __init__(self, device, *args, **kwargs):
self.device = device
super(DeviceComponentForm, self).__init__(*args, **kwargs)
# #
# Regions # Regions
# #
@@ -59,28 +70,6 @@ class RegionForm(BootstrapMixin, forms.ModelForm):
fields = ['parent', 'name', 'slug'] fields = ['parent', 'name', 'slug']
class RegionCSVForm(forms.ModelForm):
parent = forms.ModelChoiceField(
queryset=Region.objects.all(),
required=False,
to_field_name='name',
help_text='Name of parent region',
error_messages={
'invalid_choice': 'Region not found.',
}
)
class Meta:
model = Region
fields = [
'name', 'slug', 'parent',
]
help_texts = {
'name': 'Region name',
'slug': 'URL-friendly slug',
}
# #
# Sites # Sites
# #
@@ -179,27 +168,6 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm):
fields = ['site', 'name', 'slug'] fields = ['site', 'name', 'slug']
class RackGroupCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
to_field_name='name',
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Site not found.',
}
)
class Meta:
model = RackGroup
fields = [
'site', 'name', 'slug',
]
help_texts = {
'name': 'Name of rack group',
'slug': 'URL-friendly slug',
}
class RackGroupFilterForm(BootstrapMixin, forms.Form): class RackGroupFilterForm(BootstrapMixin, forms.Form):
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('rack_groups')), to_field_name='slug') site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('rack_groups')), to_field_name='slug')
@@ -216,18 +184,6 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm):
fields = ['name', 'slug', 'color'] fields = ['name', 'slug', 'color']
class RackRoleCSVForm(forms.ModelForm):
slug = SlugField()
class Meta:
model = RackRole
fields = ['name', 'slug', 'color']
help_texts = {
'name': 'Name of rack role',
'color': 'RGB color in hexadecimal (e.g. 00ff00)'
}
# #
# Racks # Racks
# #
@@ -248,8 +204,8 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'serial', 'type', 'width', 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'type', 'width', 'u_height',
'u_height', 'desc_units', 'comments', 'desc_units', 'comments',
] ]
help_texts = { help_texts = {
'site': "The site at which the rack exists", 'site': "The site at which the rack exists",
@@ -309,8 +265,7 @@ class RackCSVForm(forms.ModelForm):
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'serial', 'type', 'width', 'u_height', 'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
'desc_units',
] ]
help_texts = { help_texts = {
'name': 'Rack name', 'name': 'Rack name',
@@ -338,7 +293,6 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group') group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False) role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False)
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type') type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width') width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
u_height = forms.IntegerField(required=False, label='Height (U)') u_height = forms.IntegerField(required=False, label='Height (U)')
@@ -346,7 +300,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
comments = CommentField(widget=SmallTextarea) comments = CommentField(widget=SmallTextarea)
class Meta: class Meta:
nullable_fields = ['group', 'tenant', 'role', 'serial', 'comments'] nullable_fields = ['group', 'tenant', 'role', 'comments']
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
@@ -379,11 +333,10 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
class RackReservationForm(BootstrapMixin, forms.ModelForm): class RackReservationForm(BootstrapMixin, forms.ModelForm):
units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10})) units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
user = forms.ModelChoiceField(queryset=User.objects.order_by('username'))
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['units', 'user', 'description'] fields = ['units', 'description']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -415,15 +368,6 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
) )
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput)
user = forms.ModelChoiceField(queryset=User.objects.order_by('username'), required=False)
description = forms.CharField(max_length=100, required=False)
class Meta:
nullable_fields = []
# #
# Manufacturers # Manufacturers
# #
@@ -436,18 +380,6 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm):
fields = ['name', 'slug'] fields = ['name', 'slug']
class ManufacturerCSVForm(forms.ModelForm):
class Meta:
model = Manufacturer
fields = [
'name', 'slug'
]
help_texts = {
'name': 'Manufacturer name',
'slug': 'URL-friendly slug',
}
# #
# Device types # Device types
# #
@@ -464,46 +396,13 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
} }
class DeviceTypeCSVForm(forms.ModelForm):
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
required=True,
to_field_name='name',
help_text='Manufacturer name',
error_messages={
'invalid_choice': 'Manufacturer not found.',
}
)
subdevice_role = CSVChoiceField(
choices=SUBDEVICE_ROLE_CHOICES,
required=False,
help_text='Parent/child status'
)
interface_ordering = CSVChoiceField(
choices=IFACE_ORDERING_CHOICES,
required=False,
help_text='Interface ordering'
)
class Meta:
model = DeviceType
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
help_texts = {
'model': 'Model name',
'slug': 'URL-friendly slug',
}
class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False) manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
u_height = forms.IntegerField(min_value=1, required=False) u_height = forms.IntegerField(min_value=1, required=False)
is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth') is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False) interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
is_console_server = forms.NullBooleanField( is_console_server = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
required=False, widget=BulkEditNullBooleanSelect, label='Is a console server'
)
is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU') is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
is_network_device = forms.NullBooleanField( is_network_device = forms.NullBooleanField(
required=False, widget=BulkEditNullBooleanSelect, label='Is a network device' required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
@@ -551,7 +450,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
} }
class ConsolePortTemplateCreateForm(ComponentForm): class ConsolePortTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
@@ -565,7 +464,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
} }
class ConsoleServerPortTemplateCreateForm(ComponentForm): class ConsoleServerPortTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
@@ -579,7 +478,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
} }
class PowerPortTemplateCreateForm(ComponentForm): class PowerPortTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
@@ -593,7 +492,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
} }
class PowerOutletTemplateCreateForm(ComponentForm): class PowerOutletTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
@@ -607,7 +506,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
} }
class InterfaceTemplateCreateForm(ComponentForm): class InterfaceTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES) form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
mgmt_only = forms.BooleanField(required=False, label='OOB Management') mgmt_only = forms.BooleanField(required=False, label='OOB Management')
@@ -632,7 +531,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
} }
class DeviceBayTemplateCreateForm(ComponentForm): class DeviceBayTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
@@ -645,19 +544,7 @@ class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['name', 'slug', 'color', 'vm_role'] fields = ['name', 'slug', 'color']
class DeviceRoleCSVForm(forms.ModelForm):
slug = SlugField()
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color', 'vm_role']
help_texts = {
'name': 'Name of device role',
'color': 'RGB color in hexadecimal (e.g. 00ff00)'
}
# #
@@ -669,18 +556,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = Platform model = Platform
fields = ['name', 'slug', 'napalm_driver', 'rpc_client'] fields = ['name', 'slug', 'rpc_client']
class PlatformCSVForm(forms.ModelForm):
slug = SlugField()
class Meta:
model = Platform
fields = ['name', 'slug', 'napalm_driver']
help_texts = {
'name': 'Platform name',
}
# #
@@ -754,7 +630,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
instance = kwargs.get('instance') instance = kwargs.get('instance')
# Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field # Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
if instance and hasattr(instance, 'device_type'): if instance and hasattr(instance, 'device_type'):
initial = kwargs.get('initial', {}).copy() initial = kwargs.get('initial', {})
initial['manufacturer'] = instance.device_type.manufacturer initial['manufacturer'] = instance.device_type.manufacturer
kwargs['initial'] = initial kwargs['initial'] = initial
@@ -764,28 +640,13 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
# Compile list of choices for primary IPv4 and IPv6 addresses # Compile list of choices for primary IPv4 and IPv6 addresses
for family in [4, 6]: for family in [4, 6]:
ip_choices = [(None, '---------')] ip_choices = []
# Collect interface IPs interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
interface_ips = IPAddress.objects.select_related('interface').filter( ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
family=family, interface__device=self.instance nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
) .select_related('nat_inside__interface')
if interface_ips: ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
ip_choices.append( self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
('Interface IPs', [
(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
])
)
# Collect NAT IPs
nat_ips = IPAddress.objects.select_related('nat_inside').filter(
family=family, nat_inside__interface__device=self.instance
)
if nat_ips:
ip_choices.append(
('NAT IPs', [
(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
])
)
self.fields['primary_ip{}'.format(family)].choices = ip_choices
# If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
# can be flipped from one face to another. # can be flipped from one face to another.
@@ -914,20 +775,11 @@ class DeviceCSVForm(BaseDeviceCSVForm):
required=False, required=False,
help_text='Mounted rack face' help_text='Mounted rack face'
) )
cluster = forms.ModelChoiceField(
queryset=Cluster.objects.all(),
to_field_name='name',
required=False,
help_text='Virtualization cluster',
error_messages={
'invalid_choice': 'Invalid cluster name.',
}
)
class Meta(BaseDeviceCSVForm.Meta): class Meta(BaseDeviceCSVForm.Meta):
fields = [ fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster', 'site', 'rack_group', 'rack_name', 'position', 'face',
] ]
def clean(self): def clean(self):
@@ -963,20 +815,11 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
device_bay_name = forms.CharField( device_bay_name = forms.CharField(
help_text='Name of device bay', help_text='Name of device bay',
) )
cluster = forms.ModelChoiceField(
queryset=Cluster.objects.all(),
to_field_name='name',
required=False,
help_text='Virtualization cluster',
error_messages={
'invalid_choice': 'Invalid cluster name.',
}
)
class Meta(BaseDeviceCSVForm.Meta): class Meta(BaseDeviceCSVForm.Meta):
fields = [ fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'parent', 'device_bay_name', 'cluster', 'parent', 'device_bay_name',
] ]
def clean(self): def clean(self):
@@ -1003,11 +846,11 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role') device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False) platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
status = forms.ChoiceField(choices=add_blank_choice(STATUS_CHOICES), required=False, initial='') status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
serial = forms.CharField(max_length=50, required=False, label='Serial Number') serial = forms.CharField(max_length=50, required=False, label='Serial Number')
class Meta: class Meta:
nullable_fields = ['tenant', 'platform', 'serial'] nullable_fields = ['tenant', 'platform']
def device_status_choices(): def device_status_choices():
@@ -1067,12 +910,11 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False, initial=True) class Meta:
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU') model = Interface
mgmt_only = forms.BooleanField(required=False, label='OOB Management') fields = ['pk', 'name_pattern', 'form_factor', 'mgmt_only', 'description']
description = forms.CharField(max_length=100, required=False)
# #
@@ -1089,7 +931,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
} }
class ConsolePortCreateForm(ComponentForm): class ConsolePortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
@@ -1258,7 +1100,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
} }
class ConsoleServerPortCreateForm(ComponentForm): class ConsoleServerPortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
@@ -1350,7 +1192,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
} }
class PowerPortCreateForm(ComponentForm): class PowerPortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
@@ -1519,7 +1361,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
} }
class PowerOutletCreateForm(ComponentForm): class PowerOutletCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
@@ -1624,7 +1466,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
) )
class InterfaceCreateForm(ComponentForm): class InterfaceCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES) form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False) enabled = forms.BooleanField(required=False)
@@ -1637,15 +1479,15 @@ class InterfaceCreateForm(ComponentForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Set interfaces enabled by default # Set interfaces enabled by default
kwargs['initial'] = kwargs.get('initial', {}).copy() kwargs['initial'] = kwargs.get('initial', {})
kwargs['initial'].update({'enabled': True}) kwargs['initial'].update({'enabled': True})
super(InterfaceCreateForm, self).__init__(*args, **kwargs) super(InterfaceCreateForm, self).__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device # Limit LAG choices to interfaces belonging to this device
if self.parent is not None: if self.device is not None:
self.fields['lag'].queryset = Interface.objects.order_naturally().filter( self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
device=self.parent, form_factor=IFACE_FF_LAG device=self.device, form_factor=IFACE_FF_LAG
) )
else: else:
self.fields['lag'].queryset = Interface.objects.none() self.fields['lag'].queryset = Interface.objects.none()
@@ -1852,7 +1694,8 @@ class InterfaceConnectionCSVForm(forms.ModelForm):
return interface return interface
class InterfaceConnectionDeletionForm(ConfirmationForm): class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
confirm = forms.BooleanField(required=True)
# Used for HTTP redirect upon successful deletion # Used for HTTP redirect upon successful deletion
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False) device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
@@ -1871,7 +1714,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
} }
class DeviceBayCreateForm(ComponentForm): class DeviceBayCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')

View File

@@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-14 17:26
from __future__ import unicode_literals
from django.db import migrations, models
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):
dependencies = [
('dcim', '0040_inventoryitem_add_asset_tag_description'),
]
operations = [
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(rpc_client_to_napalm_driver),
]

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 21:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0041_napalm_integration'),
]
operations = [
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),
),
]

View File

@@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 21:26
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0042_interface_ff_10ge_cx4'),
]
operations = [
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,32 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-31 14:15
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0001_virtualization'),
('dcim', '0043_device_component_name_lengths'),
]
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'),
),
]

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-29 16:09
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0044_virtualization'),
]
operations = [
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'),
),
]

View File

@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-09 17:43
from __future__ import unicode_literals
from django.db import migrations
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0045_devicerole_vm_role'),
]
operations = [
migrations.AlterField(
model_name='rack',
name='facility_id',
field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'),
),
]

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-09 18:43
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0046_rack_lengthen_facility_id'),
]
operations = [
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),
),
]

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-09 18:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0047_more_100ge_form_factors'),
]
operations = [
migrations.AddField(
model_name='rack',
name='serial',
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
),
]

View File

@@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-10-31 17:32
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0048_rack_serial'),
]
operations = [
migrations.AlterField(
model_name='rackreservation',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,10 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from itertools import count, groupby from itertools import count, groupby
from mptt.models import MPTTModel, TreeForeignKey
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -13,10 +15,9 @@ from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist from django.db.models import Count, Q, ObjectDoesNotExist
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from mptt.models import MPTTModel, TreeForeignKey
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment from extras.models import CustomFieldModel, CustomField, CustomFieldValue, ImageAttachment
from extras.rpc import RPC_CLIENTS from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.fields import ColorField, NullableCharField from utilities.fields import ColorField, NullableCharField
@@ -25,7 +26,6 @@ from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format from utilities.utils import csv_format
from .constants import * from .constants import *
from .fields import ASNField, MACAddressField from .fields import ASNField, MACAddressField
from .querysets import InterfaceQuerySet
# #
@@ -43,10 +43,6 @@ class Region(MPTTModel):
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
csv_headers = [
'name', 'slug', 'parent',
]
class MPTTMeta: class MPTTMeta:
order_insertion_by = ['name'] order_insertion_by = ['name']
@@ -56,13 +52,6 @@ class Region(MPTTModel):
def get_absolute_url(self): def get_absolute_url(self):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug) return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
def to_csv(self):
return csv_format([
self.name,
self.slug,
self.parent.name if self.parent else None,
])
# #
# Sites # Sites
@@ -159,10 +148,6 @@ class RackGroup(models.Model):
slug = models.SlugField() slug = models.SlugField()
site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE) site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE)
csv_headers = [
'site', 'name', 'slug',
]
class Meta: class Meta:
ordering = ['site', 'name'] ordering = ['site', 'name']
unique_together = [ unique_together = [
@@ -176,13 +161,6 @@ class RackGroup(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
def to_csv(self):
return csv_format([
self.site,
self.name,
self.slug,
])
@python_2_unicode_compatible @python_2_unicode_compatible
class RackRole(models.Model): class RackRole(models.Model):
@@ -216,12 +194,11 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
Each Rack is assigned to a Site and (optionally) a RackGroup. Each Rack is assigned to a Site and (optionally) a RackGroup.
""" """
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
facility_id = NullableCharField(max_length=50, blank=True, null=True, verbose_name='Facility ID') facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT) site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL) group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT) tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT) role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT)
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type') type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type')
width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width', width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width',
help_text='Rail-to-rail width') help_text='Rail-to-rail width')
@@ -236,8 +213,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
objects = RackManager() objects = RackManager()
csv_headers = [ csv_headers = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height', 'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
'desc_units',
] ]
class Meta: class Meta:
@@ -255,8 +231,8 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
def clean(self): def clean(self):
# Validate that Rack is tall enough to house the installed Devices
if self.pk: if self.pk:
# Validate that Rack is tall enough to house the installed Devices
top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first() top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first()
if top_device: if top_device:
min_height = top_device.position + top_device.device_type.u_height - 1 min_height = top_device.position + top_device.device_type.u_height - 1
@@ -266,12 +242,6 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
min_height min_height
) )
}) })
# Validate that Rack was assigned a group of its same site, if applicable
if self.group:
if self.group.site != self.site:
raise ValidationError({
'group': "Rack group must be from the same site, {}.".format(self.site)
})
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@@ -295,7 +265,6 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
self.role.name if self.role else None, self.role.name if self.role else None,
self.get_type_display() if self.type else None, self.get_type_display() if self.type else None,
self.serial,
self.width, self.width,
self.u_height, self.u_height,
self.desc_units, self.desc_units,
@@ -388,16 +357,6 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
return list(reversed(available_units)) return list(reversed(available_units))
def get_reserved_units(self):
"""
Return a dictionary mapping all reserved units within the rack to their reservation.
"""
reserved_units = {}
for r in self.reservations.all():
for u in r.units:
reserved_units[u] = r
return reserved_units
def get_0u_devices(self): def get_0u_devices(self):
return self.devices.filter(position=0) return self.devices.filter(position=0)
@@ -417,7 +376,7 @@ class RackReservation(models.Model):
rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE) rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE)
units = ArrayField(models.PositiveSmallIntegerField()) units = ArrayField(models.PositiveSmallIntegerField())
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.PROTECT) user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT)
description = models.CharField(max_length=100) description = models.CharField(max_length=100)
class Meta: class Meta:
@@ -474,10 +433,6 @@ class Manufacturer(models.Model):
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
csv_headers = [
'name', 'slug',
]
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@@ -487,12 +442,6 @@ class Manufacturer(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug) return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
def to_csv(self):
return csv_format([
self.name,
self.slug,
])
@python_2_unicode_compatible @python_2_unicode_compatible
class DeviceType(models.Model, CustomFieldModel): class DeviceType(models.Model, CustomFieldModel):
@@ -532,11 +481,6 @@ class DeviceType(models.Model, CustomFieldModel):
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering',
]
class Meta: class Meta:
ordering = ['manufacturer', 'model'] ordering = ['manufacturer', 'model']
unique_together = [ unique_together = [
@@ -556,21 +500,6 @@ class DeviceType(models.Model, CustomFieldModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:devicetype', args=[self.pk]) return reverse('dcim:devicetype', args=[self.pk])
def to_csv(self):
return csv_format([
self.manufacturer.name,
self.model,
self.slug,
self.part_number,
self.u_height,
self.is_full_depth,
self.is_console_server,
self.is_pdu,
self.is_network_device,
self.get_subdevice_role_display() if self.subdevice_role else None,
self.get_interface_ordering_display(),
])
def clean(self): def clean(self):
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have # If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
@@ -635,7 +564,7 @@ class ConsolePortTemplate(models.Model):
A template for a ConsolePort to be created for a new Device. A template for a ConsolePort to be created for a new Device.
""" """
device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE) device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=50) name = models.CharField(max_length=30)
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@@ -651,7 +580,7 @@ class ConsoleServerPortTemplate(models.Model):
A template for a ConsoleServerPort to be created for a new Device. A template for a ConsoleServerPort to be created for a new Device.
""" """
device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE) device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=50) name = models.CharField(max_length=30)
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@@ -667,7 +596,7 @@ class PowerPortTemplate(models.Model):
A template for a PowerPort to be created for a new Device. A template for a PowerPort to be created for a new Device.
""" """
device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE) device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=50) name = models.CharField(max_length=30)
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@@ -683,7 +612,7 @@ class PowerOutletTemplate(models.Model):
A template for a PowerOutlet to be created for a new Device. A template for a PowerOutlet to be created for a new Device.
""" """
device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE) device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=50) name = models.CharField(max_length=30)
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@@ -693,13 +622,60 @@ class PowerOutletTemplate(models.Model):
return self.name return self.name
class InterfaceQuerySet(models.QuerySet):
def order_naturally(self, method=IFACE_ORDERING_POSITION):
"""
Naturally order interfaces by their type and numeric position. The sort method must be one of the defined
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
slot, subslot, position, channel, and virtual circuit:
{type}{slot}/{subslot}/{position}:{channel}.{vc}
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
be parsed as follows:
name = 'GigabitEthernet'
slot = None
subslot = 0
position = 1
channel = None
vc = 0
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
the prescribed fields.
"""
sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = {
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
}[method]
return self.extra(select={
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
}).order_by(*ordering)
def connectable(self):
"""
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
wireless).
"""
return self.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES)
@python_2_unicode_compatible @python_2_unicode_compatible
class InterfaceTemplate(models.Model): class InterfaceTemplate(models.Model):
""" """
A template for a physical data interface on a new Device. A template for a physical data interface on a new Device.
""" """
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE) device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=64) name = models.CharField(max_length=30)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
mgmt_only = models.BooleanField(default=False, verbose_name='Management only') mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
@@ -719,7 +695,7 @@ class DeviceBayTemplate(models.Model):
A template for a DeviceBay to be created for a new parent Device. A template for a DeviceBay to be created for a new parent Device.
""" """
device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE) device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=50) name = models.CharField(max_length=30)
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@@ -737,17 +713,11 @@ class DeviceBayTemplate(models.Model):
class DeviceRole(models.Model): class DeviceRole(models.Model):
""" """
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to color to be used when displaying rack elevations.
virtual machines as well.
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
color = ColorField() color = ColorField()
vm_role = models.BooleanField(
default=True,
verbose_name="VM Role",
help_text="Virtual machines may be assigned to this role"
)
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@@ -768,10 +738,7 @@ class Platform(models.Model):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
napalm_driver = models.CharField(max_length=50, blank=True, verbose_name='NAPALM driver', rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, verbose_name='RPC client')
help_text="The name of the NAPALM driver to use when interacting with devices.")
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True,
verbose_name='Legacy RPC client')
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@@ -828,13 +795,6 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True, 'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True,
verbose_name='Primary IPv6' verbose_name='Primary IPv6'
) )
cluster = models.ForeignKey(
to='virtualization.Cluster',
on_delete=models.SET_NULL,
related_name='devices',
blank=True,
null=True
)
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
images = GenericRelation(ImageAttachment) images = GenericRelation(ImageAttachment)
@@ -849,10 +809,6 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = ['rack', 'position', 'face'] unique_together = ['rack', 'position', 'face']
permissions = (
('napalm_read', 'Read-only access to devices via NAPALM'),
('napalm_write', 'Read/write access to devices via NAPALM'),
)
def __str__(self): def __str__(self):
return self.display_name or super(Device, self).__str__() return self.display_name or super(Device, self).__str__()
@@ -917,36 +873,6 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
except DeviceType.DoesNotExist: except DeviceType.DoesNotExist:
pass pass
# Validate primary IPv4 address
if self.primary_ip4 and (
self.primary_ip4.interface is None or
self.primary_ip4.interface.device != self
) and (
self.primary_ip4.nat_inside.interface is None or
self.primary_ip4.nat_inside.interface.device != self
):
raise ValidationError({
'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip4),
})
# Validate primary IPv6 address
if self.primary_ip6 and (
self.primary_ip6.interface is None or
self.primary_ip6.interface.device != self
) and (
self.primary_ip6.nat_inside.interface is None or
self.primary_ip6.nat_inside.interface.device != self
):
raise ValidationError({
'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip6),
})
# A Device can only be assigned to a Cluster in the same Site (or no Site)
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({
'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
})
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
is_new = not bool(self.pk) is_new = not bool(self.pk)
@@ -1057,7 +983,7 @@ class ConsolePort(models.Model):
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
""" """
device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=50) name = models.CharField(max_length=30)
cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL, cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL,
verbose_name='Console server port', blank=True, null=True) verbose_name='Console server port', blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED) connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
@@ -1107,7 +1033,7 @@ class ConsoleServerPort(models.Model):
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
""" """
device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=50) name = models.CharField(max_length=30)
objects = ConsoleServerPortManager() objects = ConsoleServerPortManager()
@@ -1117,17 +1043,6 @@ class ConsoleServerPort(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def clean(self):
# Check that the parent device's DeviceType is a console server
if self.device is None:
raise ValidationError("Console server ports must be assigned to devices.")
device_type = self.device.device_type
if not device_type.is_console_server:
raise ValidationError("The {} {} device type not support assignment of console server ports.".format(
device_type.manufacturer, device_type
))
# #
# Power ports # Power ports
@@ -1139,7 +1054,7 @@ class PowerPort(models.Model):
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
""" """
device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=50) name = models.CharField(max_length=30)
power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL, power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL,
blank=True, null=True) blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED) connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
@@ -1183,7 +1098,7 @@ class PowerOutlet(models.Model):
A physical power outlet (output) within a Device which provides power to a PowerPort. A physical power outlet (output) within a Device which provides power to a PowerPort.
""" """
device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE)
name = models.CharField(max_length=50) name = models.CharField(max_length=30)
objects = PowerOutletManager() objects = PowerOutletManager()
@@ -1193,17 +1108,6 @@ class PowerOutlet(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def clean(self):
# Check that the parent device's DeviceType is a PDU
if self.device is None:
raise ValidationError("Power outlets must be assigned to devices.")
device_type = self.device.device_type
if not device_type.is_pdu:
raise ValidationError("The {} {} device type not support assignment of power outlets.".format(
device_type.manufacturer, device_type
))
# #
# Interfaces # Interfaces
@@ -1212,32 +1116,19 @@ class PowerOutlet(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Interface(models.Model): class Interface(models.Model):
""" """
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
Interface via the creation of an InterfaceConnection. of an InterfaceConnection.
""" """
device = models.ForeignKey( device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
to='Device',
on_delete=models.CASCADE,
related_name='interfaces',
null=True,
blank=True
)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='interfaces',
null=True,
blank=True
)
lag = models.ForeignKey( lag = models.ForeignKey(
to='self', 'self',
on_delete=models.SET_NULL, models.SET_NULL,
related_name='member_interfaces', related_name='member_interfaces',
null=True, null=True,
blank=True, blank=True,
verbose_name='Parent LAG' verbose_name='Parent LAG'
) )
name = models.CharField(max_length=64) name = models.CharField(max_length=30)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address') mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
@@ -1260,26 +1151,6 @@ class Interface(models.Model):
def clean(self): def clean(self):
# Check that the parent device's DeviceType is a network device
if self.device is not None:
device_type = self.device.device_type
if not device_type.is_network_device:
raise ValidationError("The {} {} device type not support assignment of network interfaces.".format(
device_type.manufacturer, device_type
))
# An Interface must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine:
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
if not self.device and not self.virtual_machine:
raise ValidationError("An interface must belong to either a device or a virtual machine.")
# VM interfaces must be virtual
if self.virtual_machine and self.form_factor is not IFACE_FF_VIRTUAL:
raise ValidationError({
'form_factor': "Virtual machines can only have virtual interfaces."
})
# Virtual interfaces cannot be connected # Virtual interfaces cannot be connected
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected: if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
raise ValidationError({ raise ValidationError({
@@ -1309,10 +1180,6 @@ class Interface(models.Model):
) )
}) })
@property
def parent(self):
return self.device or self.virtual_machine
@property @property
def is_virtual(self): def is_virtual(self):
return self.form_factor in VIRTUAL_IFACE_TYPES return self.form_factor in VIRTUAL_IFACE_TYPES

View File

@@ -1,72 +0,0 @@
from __future__ import unicode_literals
from django.db.models import QuerySet
from django.db.models.expressions import RawSQL
from .constants import IFACE_ORDERING_NAME, IFACE_ORDERING_POSITION, NONCONNECTABLE_IFACE_TYPES
class InterfaceQuerySet(QuerySet):
def order_naturally(self, method=IFACE_ORDERING_POSITION):
"""
Naturally order interfaces by their type and numeric position. The sort method must be one of the defined
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
slot, subslot, position, channel, and virtual circuit:
{type}{slot}/{subslot}/{position}/{subposition}:{channel}.{vc}
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet1/2/3
would be parsed as follows:
name = 'GigabitEthernet'
slot = 1
subslot = 2
position = 3
subposition = 0
channel = None
vc = 0
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
the prescribed fields.
"""
sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = {
IFACE_ORDERING_POSITION: (
'_slot', '_subslot', '_position', '_subposition', '_channel', '_type', '_vc', '_id', 'name',
),
IFACE_ORDERING_NAME: (
'_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_id', 'name',
),
}[method]
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
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), []),
'_id': RawSQL(ID_RE.format(sql_col), []),
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
'_position': RawSQL(POSITION_RE.format(sql_col), []),
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
'_vc': RawSQL(VC_RE.format(sql_col), []),
}
return self.annotate(**fields).order_by(*ordering)
def connectable(self):
"""
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
wireless).
"""
return self.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES)

View File

@@ -3,13 +3,14 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site, RackGroup, RackReservation, Region, Site,
) )
REGION_LINK = """ REGION_LINK = """
{% if record.get_children %} {% if record.get_children %}
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i> <span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
@@ -141,26 +142,30 @@ class SiteTable(BaseTable):
name = tables.LinkColumn() name = tables.LinkColumn()
region = tables.TemplateColumn(template_code=SITE_REGION_LINK) region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(BaseTable.Meta):
model = Site
fields = ('pk', 'name', 'facility', 'region', 'tenant', 'asn')
class SiteDetailTable(SiteTable):
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks') rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices') device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes') prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs') vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits') circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
class Meta(SiteTable.Meta): class Meta(BaseTable.Meta):
model = Site
fields = ( fields = (
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count', 'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count', 'vlan_count', 'circuit_count',
) )
class SiteSearchTable(SearchTable):
name = tables.LinkColumn()
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(SearchTable.Meta):
model = Site
fields = ('name', 'facility', 'region', 'tenant', 'asn')
# #
# Rack groups # Rack groups
# #
@@ -209,22 +214,29 @@ class RackTable(BaseTable):
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
role = tables.TemplateColumn(RACK_ROLE) role = tables.TemplateColumn(RACK_ROLE)
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
class Meta(BaseTable.Meta):
model = Rack
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height')
class RackDetailTable(RackTable):
devices = tables.Column(accessor=Accessor('device_count')) devices = tables.Column(accessor=Accessor('device_count'))
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(RackTable.Meta): class Meta(BaseTable.Meta):
model = Rack
fields = ( fields = (
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization' 'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
) )
class RackSearchTable(SearchTable):
name = tables.LinkColumn()
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
role = tables.TemplateColumn(RACK_ROLE)
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
class Meta(SearchTable.Meta):
model = Rack
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height')
class RackImportTable(BaseTable): class RackImportTable(BaseTable):
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name') name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
@@ -290,7 +302,23 @@ class DeviceTypeTable(BaseTable):
model = DeviceType model = DeviceType
fields = ( fields = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu', 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role', 'instance_count', 'is_network_device', 'subdevice_role', 'instance_count'
)
class DeviceTypeSearchTable(SearchTable):
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')
class Meta(SearchTable.Meta):
model = DeviceType
fields = (
'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role',
) )
@@ -305,6 +333,7 @@ class ConsolePortTemplateTable(BaseTable):
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
class ConsoleServerPortTemplateTable(BaseTable): class ConsoleServerPortTemplateTable(BaseTable):
@@ -314,6 +343,7 @@ class ConsoleServerPortTemplateTable(BaseTable):
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
class PowerPortTemplateTable(BaseTable): class PowerPortTemplateTable(BaseTable):
@@ -323,6 +353,7 @@ class PowerPortTemplateTable(BaseTable):
model = PowerPortTemplate model = PowerPortTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
class PowerOutletTemplateTable(BaseTable): class PowerOutletTemplateTable(BaseTable):
@@ -332,6 +363,7 @@ class PowerOutletTemplateTable(BaseTable):
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
class InterfaceTemplateTable(BaseTable): class InterfaceTemplateTable(BaseTable):
@@ -342,6 +374,7 @@ class InterfaceTemplateTable(BaseTable):
model = InterfaceTemplate model = InterfaceTemplate
fields = ('pk', 'name', 'mgmt_only', 'form_factor') fields = ('pk', 'name', 'mgmt_only', 'form_factor')
empty_text = "None" empty_text = "None"
show_header = False
class DeviceBayTemplateTable(BaseTable): class DeviceBayTemplateTable(BaseTable):
@@ -351,6 +384,7 @@ class DeviceBayTemplateTable(BaseTable):
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
# #
@@ -361,15 +395,14 @@ class DeviceRoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices') device_count = tables.Column(verbose_name='Devices')
vm_count = tables.Column(verbose_name='VMs') color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='') verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions') fields = ('pk', 'name', 'device_count', 'color', 'slug', 'actions')
# #
@@ -381,12 +414,13 @@ class PlatformTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices') device_count = tables.Column(verbose_name='Devices')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
rpc_client = tables.Column(accessor='get_rpc_client_display', orderable=False, verbose_name='RPC Client')
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='') verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
fields = ('pk', 'name', 'device_count', 'slug', 'napalm_driver', 'actions') fields = ('pk', 'name', 'device_count', 'slug', 'rpc_client', 'actions')
# #
@@ -405,22 +439,32 @@ class DeviceTable(BaseTable):
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', 'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
text=lambda record: record.device_type.full_name text=lambda record: record.device_type.full_name
) )
class Meta(BaseTable.Meta):
model = Device
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
class DeviceDetailTable(DeviceTable):
primary_ip = tables.TemplateColumn( primary_ip = tables.TemplateColumn(
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
) )
class Meta(DeviceTable.Meta): class Meta(BaseTable.Meta):
model = Device model = Device
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip') fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
class DeviceSearchTable(SearchTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK)
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
device_type = tables.LinkColumn(
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
text=lambda record: record.device_type.full_name
)
class Meta(SearchTable.Meta):
model = Device
fields = ('name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
class DeviceImportTable(BaseTable): class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status') status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
@@ -437,52 +481,6 @@ class DeviceImportTable(BaseTable):
empty_text = False empty_text = False
#
# Device components
#
class ConsolePortTable(BaseTable):
class Meta(BaseTable.Meta):
model = ConsolePort
fields = ('name',)
class ConsoleServerPortTable(BaseTable):
class Meta(BaseTable.Meta):
model = ConsoleServerPort
fields = ('name',)
class PowerPortTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('name',)
class PowerOutletTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerOutlet
fields = ('name',)
class InterfaceTable(BaseTable):
class Meta(BaseTable.Meta):
model = Interface
fields = ('name', 'form_factor', 'lag', 'enabled', 'mgmt_only', 'description')
class DeviceBayTable(BaseTable):
class Meta(BaseTable.Meta):
model = DeviceBay
fields = ('name',)
# #
# Device connections # Device connections
# #

View File

@@ -1,16 +1,16 @@
from __future__ import unicode_literals 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 import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from dcim.constants import IFACE_FF_LAG, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT from django.contrib.auth.models import User
from django.urls import reverse
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup, Manufacturer, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, RackReservation, RackRole, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
) )
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from users.models import Token from users.models import Token
@@ -1432,7 +1432,7 @@ class ConsoleServerPortTest(HttpStatusMixin, APITestCase):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_console_server=True manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -1590,7 +1590,7 @@ class PowerOutletTest(HttpStatusMixin, APITestCase):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_pdu=True manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -1667,7 +1667,7 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_network_device=True manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'

View File

@@ -9,29 +9,14 @@ class RackTestCase(TestCase):
def setUp(self): def setUp(self):
self.site1 = Site.objects.create( self.site = Site.objects.create(
name='TestSite1', name='TestSite1',
slug='test-site-1' slug='my-test-site'
)
self.site2 = Site.objects.create(
name='TestSite2',
slug='test-site-2'
)
self.group1 = RackGroup.objects.create(
name='TestGroup1',
slug='test-group-1',
site=self.site1
)
self.group2 = RackGroup.objects.create(
name='TestGroup2',
slug='test-group-2',
site=self.site2
) )
self.rack = Rack.objects.create( self.rack = Rack.objects.create(
name='TestRack1', name='TestRack1',
facility_id='A101', facility_id='A101',
site=self.site1, site=self.site,
group=self.group1,
u_height=42 u_height=42
) )
self.manufacturer = Manufacturer.objects.create( self.manufacturer = Manufacturer.objects.create(
@@ -72,51 +57,13 @@ class RackTestCase(TestCase):
} }
def test_rack_device_outside_height(self):
rack1 = Rack(
name='TestRack2',
facility_id='A102',
site=self.site1,
u_height=42
)
rack1.save()
device1 = Device(
name='TestSwitch1',
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
device_role=DeviceRole.objects.get(slug='switch'),
site=self.site1,
rack=rack1,
position=43,
face=RACK_FACE_FRONT,
)
device1.save()
with self.assertRaises(ValidationError):
rack1.clean()
def test_rack_group_site(self):
rack_invalid_group = Rack(
name='TestRack2',
facility_id='A102',
site=self.site1,
u_height=42,
group=self.group2
)
rack_invalid_group.save()
with self.assertRaises(ValidationError):
rack_invalid_group.clean()
def test_mount_single_device(self): def test_mount_single_device(self):
device1 = Device( device1 = Device(
name='TestSwitch1', name='TestSwitch1',
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'), device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
device_role=DeviceRole.objects.get(slug='switch'), device_role=DeviceRole.objects.get(slug='switch'),
site=self.site1, site=self.site,
rack=self.rack, rack=self.rack,
position=10, position=10,
face=RACK_FACE_REAR, face=RACK_FACE_REAR,
@@ -145,118 +92,9 @@ class RackTestCase(TestCase):
name='TestPDU', name='TestPDU',
device_role=self.role.get('PDU'), device_role=self.role.get('PDU'),
device_type=self.device_type.get('cc5000'), device_type=self.device_type.get('cc5000'),
site=self.site1, site=self.site,
rack=self.rack, rack=self.rack,
position=None, position=None,
face=None, face=None,
) )
self.assertTrue(pdu) self.assertTrue(pdu)
class InterfaceTestCase(TestCase):
def setUp(self):
self.site = Site.objects.create(
name='TestSite1',
slug='my-test-site'
)
self.rack = Rack.objects.create(
name='TestRack1',
facility_id='A101',
site=self.site,
u_height=42
)
self.manufacturer = Manufacturer.objects.create(
name='Acme',
slug='acme'
)
self.device_type = DeviceType.objects.create(
manufacturer=self.manufacturer,
model='FrameForwarder 2048',
slug='ff2048'
)
self.role = DeviceRole.objects.create(
name='Switch',
slug='switch',
)
def test_interface_order_natural(self):
device1 = Device.objects.create(
name='TestSwitch1',
device_type=self.device_type,
device_role=self.role,
site=self.site,
rack=self.rack,
position=10,
face=RACK_FACE_REAR,
)
interface1 = Interface.objects.create(
device=device1,
name='Ethernet1/3/1'
)
interface2 = Interface.objects.create(
device=device1,
name='Ethernet1/5/1'
)
interface3 = Interface.objects.create(
device=device1,
name='Ethernet1/4'
)
interface4 = Interface.objects.create(
device=device1,
name='Ethernet1/3/2/4'
)
interface5 = Interface.objects.create(
device=device1,
name='Ethernet1/3/2/1'
)
interface6 = Interface.objects.create(
device=device1,
name='Loopback1'
)
self.assertEqual(
list(Interface.objects.all().order_naturally()),
[interface1, interface5, interface4, interface3, interface2, interface6]
)
def test_interface_order_natural_subinterfaces(self):
device1 = Device.objects.create(
name='TestSwitch1',
device_type=self.device_type,
device_role=self.role,
site=self.site,
rack=self.rack,
position=10,
face=RACK_FACE_REAR,
)
interface1 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/3'
)
interface2 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/2.2'
)
interface3 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/0.120'
)
interface4 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/0'
)
interface5 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/1.117'
)
interface6 = Interface.objects.create(
device=device1,
name='GigabitEthernet0'
)
self.assertEqual(
list(Interface.objects.all().order_naturally()),
[interface4, interface3, interface5, interface2, interface1, interface6]
)

View File

@@ -5,8 +5,9 @@ from django.conf.urls import url
from extras.views import ImageAttachmentEditView from extras.views import ImageAttachmentEditView
from ipam.views import ServiceCreateView from ipam.views import ServiceCreateView
from secrets.views import secret_add from secrets.views import secret_add
from . import views
from .models import Device, Rack, Site from .models import Device, Rack, Site
from . import views
app_name = 'dcim' app_name = 'dcim'
urlpatterns = [ urlpatterns = [
@@ -14,7 +15,6 @@ urlpatterns = [
# Regions # Regions
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'), url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'), url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'),
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/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+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
@@ -31,20 +31,17 @@ urlpatterns = [
# Rack groups # Rack groups
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'), url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'), url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
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/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+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
# Rack roles # Rack roles
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'), url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'), url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'),
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/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+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
# Rack reservations # Rack reservations
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'), url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
url(r'^rack-reservations/edit/$', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), 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+)/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+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
@@ -65,14 +62,12 @@ urlpatterns = [
# Manufacturers # Manufacturers
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'), url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
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/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-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
# Device types # Device types
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'), url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
url(r'^device-types/import/$', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'), url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
@@ -107,14 +102,12 @@ urlpatterns = [
# Device roles # Device roles
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'), url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
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/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-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
# Platforms # Platforms
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'), url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'), url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'),
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/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-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
@@ -129,11 +122,9 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), 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+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), 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'), url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'), url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='device_service_assign'), url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'),
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
# Console ports # Console ports

File diff suppressed because it is too large Load Diff

View File

@@ -7,18 +7,6 @@ from django.utils.safestring import mark_safe
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction
def order_content_types(field):
"""
Order the list of available ContentTypes by application
"""
queryset = field.queryset.order_by('app_label', 'model')
field.choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
#
# Custom fields
#
class CustomFieldForm(forms.ModelForm): class CustomFieldForm(forms.ModelForm):
class Meta: class Meta:
@@ -28,7 +16,9 @@ class CustomFieldForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CustomFieldForm, self).__init__(*args, **kwargs) super(CustomFieldForm, self).__init__(*args, **kwargs)
order_content_types(self.fields['obj_type']) # Organize the available ContentTypes
queryset = self.fields['obj_type'].queryset.order_by('app_label', 'model')
self.fields['obj_type'].choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
class CustomFieldChoiceAdmin(admin.TabularInline): class CustomFieldChoiceAdmin(admin.TabularInline):
@@ -46,43 +36,16 @@ class CustomFieldAdmin(admin.ModelAdmin):
return ', '.join([ct.name for ct in obj.obj_type.all()]) return ', '.join([ct.name for ct in obj.obj_type.all()])
#
# Graphs
#
@admin.register(Graph) @admin.register(Graph)
class GraphAdmin(admin.ModelAdmin): class GraphAdmin(admin.ModelAdmin):
list_display = ['name', 'type', 'weight', 'source'] list_display = ['name', 'type', 'weight', 'source']
#
# Export templates
#
class ExportTemplateForm(forms.ModelForm):
class Meta:
model = ExportTemplate
exclude = []
def __init__(self, *args, **kwargs):
super(ExportTemplateForm, self).__init__(*args, **kwargs)
# Format ContentType choices
order_content_types(self.fields['content_type'])
self.fields['content_type'].choices.insert(0, ('', '---------'))
@admin.register(ExportTemplate) @admin.register(ExportTemplate)
class ExportTemplateAdmin(admin.ModelAdmin): class ExportTemplateAdmin(admin.ModelAdmin):
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension'] list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
form = ExportTemplateForm
#
# Topology maps
#
@admin.register(TopologyMap) @admin.register(TopologyMap)
class TopologyMapAdmin(admin.ModelAdmin): class TopologyMapAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'site'] list_display = ['name', 'slug', 'site']
@@ -91,10 +54,6 @@ class TopologyMapAdmin(admin.ModelAdmin):
} }
#
# User actions
#
@admin.register(UserAction) @admin.register(UserAction)
class UserActionAdmin(admin.ModelAdmin): class UserActionAdmin(admin.ModelAdmin):
actions = None actions = None

View File

@@ -1,15 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import datetime from datetime import datetime
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT from django.contrib.contenttypes.models import ContentType
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue from django.db import transaction
from utilities.api import ValidatedModelSerializer
from extras.models import (
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
)
# #
@@ -28,56 +28,34 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
for field_name, value in data.items(): for field_name, value in data.items():
try: cf = custom_fields[field_name]
cf = custom_fields[field_name]
except KeyError:
raise ValidationError(
"Invalid custom field for {} objects: {}".format(content_type, field_name)
)
# Data validation # Validate custom field name
if value not in [None, '']: if field_name not in custom_fields:
raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name))
# Validate integer # Validate boolean
if cf.type == CF_TYPE_INTEGER: if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
try: raise ValidationError("Invalid value for boolean field {}: {}".format(field_name, value))
int(value)
except ValueError:
raise ValidationError(
"Invalid value for integer field {}: {}".format(field_name, value)
)
# Validate boolean # Validate date
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]: if cf.type == CF_TYPE_DATE:
raise ValidationError( try:
"Invalid value for boolean field {}: {}".format(field_name, value) datetime.strptime(value, '%Y-%m-%d')
) except ValueError:
raise ValidationError("Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(
field_name, value
))
# Validate date # Validate selected choice
if cf.type == CF_TYPE_DATE: if cf.type == CF_TYPE_SELECT:
try: try:
datetime.strptime(value, '%Y-%m-%d') value = int(value)
except ValueError: except ValueError:
raise ValidationError( raise ValidationError("{}: Choice selections must be passed as integers.".format(field_name))
"Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(field_name, value) valid_choices = [c.pk for c in cf.choices.all()]
) if value not in valid_choices:
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))
# Validate selected choice
if cf.type == CF_TYPE_SELECT:
try:
value = int(value)
except ValueError:
raise ValidationError(
"{}: Choice selections must be passed as integers.".format(field_name)
)
valid_choices = [c.pk for c in cf.choices.all()]
if value not in valid_choices:
raise ValidationError(
"Invalid choice for field {}: {}".format(field_name, value)
)
elif cf.required:
raise ValidationError("Required field {} cannot be empty.".format(field_name))
# Check for missing required fields # Check for missing required fields
missing_fields = [] missing_fields = []
@@ -90,7 +68,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
return data return data
class CustomFieldModelSerializer(ValidatedModelSerializer): class CustomFieldModelSerializer(serializers.ModelSerializer):
""" """
Extends ModelSerializer to render any CustomFields and their values associated with an object. Extends ModelSerializer to render any CustomFields and their values associated with an object.
""" """
@@ -133,6 +111,16 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
defaults={'serialized_value': custom_field.serialize_value(value)}, defaults={'serialized_value': custom_field.serialize_value(value)},
) )
def validate(self, data):
"""
Enforce model validation (see utilities.api.ModelValidationMixin)
"""
model_data = data.copy()
model_data.pop('custom_fields', None)
instance = self.Meta.model(**model_data)
instance.clean()
return data
def create(self, validated_data): def create(self, validated_data):
custom_fields = validated_data.pop('custom_fields', None) custom_fields = validated_data.pop('custom_fields', None)

View File

@@ -1,14 +1,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers from rest_framework import serializers
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
from dcim.models import Device, Rack, Site from dcim.models import Device, Rack, Site
from extras.constants import ACTION_CHOICES, GRAPH_TYPE_CHOICES from extras.models import (
from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
)
from users.api.serializers import NestedUserSerializer from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin
# #
@@ -102,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
return serializer(obj.parent, context={'request': self.context['request']}).data return serializer(obj.parent, context={'request': self.context['request']}).data
class WritableImageAttachmentSerializer(ValidatedModelSerializer): class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
content_type = ContentTypeFieldSerializer() content_type = ContentTypeFieldSerializer()
class Meta: class Meta:
@@ -125,41 +127,6 @@ class WritableImageAttachmentSerializer(ValidatedModelSerializer):
return data return data
#
# Reports
#
class ReportResultSerializer(serializers.ModelSerializer):
class Meta:
model = ReportResult
fields = ['created', 'user', 'failed', 'data']
class NestedReportResultSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='extras-api:report-detail',
lookup_field='report',
lookup_url_kwarg='pk'
)
class Meta:
model = ReportResult
fields = ['url', 'created', 'user', 'failed']
class ReportSerializer(serializers.Serializer):
module = serializers.CharField(max_length=255)
name = serializers.CharField(max_length=255)
description = serializers.CharField(max_length=255, required=False)
test_methods = serializers.ListField(child=serializers.CharField(max_length=255))
result = NestedReportResultSerializer()
class ReportDetailSerializer(ReportSerializer):
result = ReportResultSerializer()
# #
# User actions # User actions
# #

View File

@@ -16,9 +16,6 @@ class ExtrasRootView(routers.APIRootView):
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.APIRootView = ExtrasRootView router.APIRootView = ExtrasRootView
# Field choices
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, base_name='field-choice')
# Graphs # Graphs
router.register(r'graphs', views.GraphViewSet) router.register(r'graphs', views.GraphViewSet)
@@ -31,9 +28,6 @@ router.register(r'topology-maps', views.TopologyMapViewSet)
# Image attachments # Image attachments
router.register(r'image-attachments', views.ImageAttachmentViewSet) router.register(r'image-attachments', views.ImageAttachmentViewSet)
# Reports
router.register(r'reports', views.ReportViewSet, base_name='report')
# Recent activity # Recent activity
router.register(r'recent-activity', views.RecentActivityViewSet) router.register(r'recent-activity', views.RecentActivityViewSet)

View File

@@ -1,35 +1,18 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from extras import filters from extras import filters
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction from extras.models import ExportTemplate, Graph, ImageAttachment, TopologyMap, UserAction
from extras.reports import get_report, get_reports from utilities.api import WritableSerializerMixin
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, WritableSerializerMixin
from . import serializers from . import serializers
#
# Field choices
#
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(CustomField, ['type']),
(Graph, ['type']),
)
#
# Custom fields
#
class CustomFieldModelViewSet(ModelViewSet): class CustomFieldModelViewSet(ModelViewSet):
""" """
Include the applicable set of CustomFields in the ModelViewSet context. Include the applicable set of CustomFields in the ModelViewSet context.
@@ -60,10 +43,6 @@ class CustomFieldModelViewSet(ModelViewSet):
return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field') return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field')
#
# Graphs
#
class GraphViewSet(WritableSerializerMixin, ModelViewSet): class GraphViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Graph.objects.all() queryset = Graph.objects.all()
serializer_class = serializers.GraphSerializer serializer_class = serializers.GraphSerializer
@@ -71,20 +50,12 @@ class GraphViewSet(WritableSerializerMixin, ModelViewSet):
filter_class = filters.GraphFilter filter_class = filters.GraphFilter
#
# Export templates
#
class ExportTemplateViewSet(WritableSerializerMixin, ModelViewSet): class ExportTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ExportTemplate.objects.all() queryset = ExportTemplate.objects.all()
serializer_class = serializers.ExportTemplateSerializer serializer_class = serializers.ExportTemplateSerializer
filter_class = filters.ExportTemplateFilter filter_class = filters.ExportTemplateFilter
#
# Topology maps
#
class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet): class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
queryset = TopologyMap.objects.select_related('site') queryset = TopologyMap.objects.select_related('site')
serializer_class = serializers.TopologyMapSerializer serializer_class = serializers.TopologyMapSerializer
@@ -111,96 +82,12 @@ class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
return response return response
#
# Image attachments
#
class ImageAttachmentViewSet(WritableSerializerMixin, ModelViewSet): class ImageAttachmentViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ImageAttachment.objects.all() queryset = ImageAttachment.objects.all()
serializer_class = serializers.ImageAttachmentSerializer serializer_class = serializers.ImageAttachmentSerializer
write_serializer_class = serializers.WritableImageAttachmentSerializer write_serializer_class = serializers.WritableImageAttachmentSerializer
#
# Reports
#
class ReportViewSet(ViewSet):
permission_classes = [IsAuthenticatedOrLoginNotRequired]
_ignore_model_permissions = True
exclude_from_schema = True
lookup_value_regex = '[^/]+' # Allow dots
def _retrieve_report(self, pk):
# Read the PK as "<module>.<report>"
if '.' not in pk:
raise Http404
module_name, report_name = pk.split('.', 1)
# Raise a 404 on an invalid Report module/name
report = get_report(module_name, report_name)
if report is None:
raise Http404
return report
def list(self, request):
"""
Compile all reports and their related results (if any). Result data is deferred in the list view.
"""
report_list = []
# Iterate through all available Reports.
for module_name, reports in get_reports():
for report in reports:
# Attach the relevant ReportResult (if any) to each Report.
report.result = ReportResult.objects.filter(report=report.full_name).defer('data').first()
report_list.append(report)
serializer = serializers.ReportSerializer(report_list, many=True, context={
'request': request,
})
return Response(serializer.data)
def retrieve(self, request, pk):
"""
Retrieve a single Report identified as "<module>.<report>".
"""
# Retrieve the Report and ReportResult, if any.
report = self._retrieve_report(pk)
report.result = ReportResult.objects.filter(report=report.full_name).first()
serializer = serializers.ReportDetailSerializer(report)
return Response(serializer.data)
@detail_route(methods=['post'])
def run(self, request, pk):
"""
Run a Report and create a new ReportResult, overwriting any previous result for the Report.
"""
# Check that the user has permission to run reports.
if not request.user.has_perm('extras.add_reportresult'):
raise PermissionDenied("This user does not have permission to run reports.")
# Retrieve and run the Report. This will create a new ReportResult.
report = self._retrieve_report(pk)
report.run()
serializer = serializers.ReportDetailSerializer(report)
return Response(serializer.data)
#
# User activity
#
class RecentActivityViewSet(ReadOnlyModelViewSet): class RecentActivityViewSet(ReadOnlyModelViewSet):
""" """
List all UserActions to provide a log of recent activity. List all UserActions to provide a log of recent activity.

View File

@@ -3,11 +3,10 @@ from __future__ import unicode_literals
# Models which support custom fields # Models which support custom fields
CUSTOMFIELD_MODELS = ( CUSTOMFIELD_MODELS = (
'provider', 'circuit', # Circuits
'site', 'rack', 'devicetype', 'device', # DCIM 'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'tenant', # Tenancy 'provider', 'circuit', # Circuits
'cluster', 'virtualmachine', # Virtualization 'tenant', # Tenants
) )
# Custom field types # Custom field types
@@ -38,12 +37,10 @@ GRAPH_TYPE_CHOICES = (
# Models which support export templates # Models which support export templates
EXPORTTEMPLATE_MODELS = [ EXPORTTEMPLATE_MODELS = [
'provider', 'circuit', # Circuits 'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM
'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM 'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'tenant', # Tenancy 'provider', 'circuit', # Circuits
'cluster', 'virtualmachine', # Virtualization 'tenant', # Tenants
] ]
# User action types # User action types
@@ -63,17 +60,3 @@ ACTION_CHOICES = (
(ACTION_DELETE, 'deleted'), (ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'), (ACTION_BULK_DELETE, 'bulk deleted'),
) )
# Report logging levels
LOG_DEFAULT = 0
LOG_SUCCESS = 10
LOG_INFO = 20
LOG_WARNING = 30
LOG_FAILURE = 40
LOG_LEVEL_CODES = {
LOG_DEFAULT: 'default',
LOG_SUCCESS: 'success',
LOG_INFO: 'info',
LOG_WARNING: 'warning',
LOG_FAILURE: 'failure',
}

View File

@@ -1,12 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from dcim.models import Site from dcim.models import Site
from .constants import CF_TYPE_SELECT from .models import CF_TYPE_SELECT, CustomField, Graph, ExportTemplate, TopologyMap, UserAction
from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
class CustomFieldFilter(django_filters.Filter): class CustomFieldFilter(django_filters.Filter):
@@ -19,28 +19,17 @@ class CustomFieldFilter(django_filters.Filter):
super(CustomFieldFilter, self).__init__(*args, **kwargs) super(CustomFieldFilter, self).__init__(*args, **kwargs)
def filter(self, queryset, value): def filter(self, queryset, value):
# Skip filter on empty value # Skip filter on empty value
if not value.strip(): if not value.strip():
return queryset return queryset
# Treat 0 as None for Select fields
# Selection fields get special treatment (values must be integers) try:
if self.cf_type == CF_TYPE_SELECT: if self.cf_type == CF_TYPE_SELECT and int(value) == 0:
try: return queryset.exclude(
# Treat 0 as None custom_field_values__field__name=self.name,
if int(value) == 0: )
return queryset.exclude( except ValueError:
custom_field_values__field__name=self.name, pass
)
# Match on exact CustomFieldChoice PK
else:
return queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__serialized_value=value,
)
except ValueError:
return queryset.none()
return queryset.filter( return queryset.filter(
custom_field_values__field__name=self.name, custom_field_values__field__name=self.name,
custom_field_values__serialized_value__icontains=value, custom_field_values__serialized_value__icontains=value,

View File

@@ -1,13 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
from .constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL from .models import (
from .models import CustomField, CustomFieldValue, ImageAttachment CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue,
ImageAttachment,
)
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False): def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):

View File

@@ -10,7 +10,8 @@ from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Model from django.db.models import Model
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users']
BANNER_TEXT = """### NetBox interactive shell ({node}) BANNER_TEXT = """### NetBox interactive shell ({node})
### Python {python} | Django {django} | NetBox {netbox} ### Python {python} | Django {django} | NetBox {netbox}
@@ -36,29 +37,19 @@ class Command(BaseCommand):
def get_namespace(self): def get_namespace(self):
namespace = {} namespace = {}
# Gather Django models and constants from each app # Gather Django models from each app
for app in APPS: for app in APPS:
self.django_models[app] = [] self.django_models[app] = []
# Models
app_models = sys.modules['{}.models'.format(app)] app_models = sys.modules['{}.models'.format(app)]
for name in dir(app_models): for name in dir(app_models):
model = getattr(app_models, name) model = getattr(app_models, name)
try: try:
if issubclass(model, Model) and model._meta.app_label == app: if issubclass(model, Model):
namespace[name] = model namespace[name] = model
self.django_models[app].append(name) self.django_models[app].append(name)
except TypeError: except TypeError:
pass pass
# Constants
try:
app_constants = sys.modules['{}.constants'.format(app)]
for name in dir(app_constants):
namespace[name] = getattr(app_constants, name)
except KeyError:
pass
# Load convenience commands # Load convenience commands
namespace.update({ namespace.update({
'lsmodels': self._lsmodels, 'lsmodels': self._lsmodels,

View File

@@ -1,20 +1,20 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from getpass import getpass from getpass import getpass
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db import transaction from django.db import transaction
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
class Command(BaseCommand): class Command(BaseCommand):
help = "Update inventory information for specified devices" help = "Update inventory information for specified devices"
username = settings.NAPALM_USERNAME username = settings.NETBOX_USERNAME
password = settings.NAPALM_PASSWORD password = settings.NETBOX_PASSWORD
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('-u', '--username', dest='username', help="Specify the username to use") parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")

View File

@@ -1,46 +0,0 @@
from __future__ import unicode_literals
from django.core.management.base import BaseCommand
from django.utils import timezone
from extras.reports import get_reports
class Command(BaseCommand):
help = "Run a report to validate data in NetBox"
def add_arguments(self, parser):
parser.add_argument('reports', nargs='+', help="Report(s) to run")
def handle(self, *args, **options):
# Gather all available reports
reports = get_reports()
# Run reports
for module_name, report_list in reports:
for report in report_list:
if module_name in options['reports'] or report.full_name in options['reports']:
# Run the report and create a new ReportResult
self.stdout.write(
"[{:%H:%M:%S}] Running {}...".format(timezone.now(), report.full_name)
)
report.run()
# Report on success/failure
status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
for test_name, attrs in report.result.data.items():
self.stdout.write(
"\t{}: {} success, {} info, {} warning, {} failure".format(
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failure']
)
)
self.stdout.write(
"[{:%H:%M:%S}] {}: {}".format(timezone.now(), report.full_name, status)
)
# Wrap things up
self.stdout.write(
"[{:%H:%M:%S}] Finished".format(timezone.now())
)

View File

@@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-26 21:25
from __future__ import unicode_literals
from distutils.version import StrictVersion
import re
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import connection, migrations, models
import django.db.models.deletion
from django.db.utils import OperationalError
def verify_postgresql_version(apps, schema_editor):
"""
Verify that PostgreSQL is version 9.4 or higher.
"""
try:
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
row = cursor.fetchone()
pg_version = re.match('^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
except OperationalError:
pass
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('extras', '0007_unicode_literals'),
]
operations = [
migrations.RunPython(verify_postgresql_version),
migrations.CreateModel(
name='ReportResult',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('report', models.CharField(max_length=255, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
('failed', models.BooleanField()),
('data', django.contrib.postgres.fields.jsonb.JSONField()),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['report'],
},
),
]

View File

@@ -1,13 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
import graphviz import graphviz
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
@@ -275,7 +273,6 @@ class TopologyMap(models.Model):
# Construct the graph # Construct the graph
graph = graphviz.Graph() graph = graphviz.Graph()
graph.graph_attr['ranksep'] = '1' graph.graph_attr['ranksep'] = '1'
seen = set()
for i, device_set in enumerate(self.device_sets): for i, device_set in enumerate(self.device_sets):
subgraph = graphviz.Graph(name='sg{}'.format(i)) subgraph = graphviz.Graph(name='sg{}'.format(i))
@@ -288,11 +285,8 @@ class TopologyMap(models.Model):
# Add each device to the graph # Add each device to the graph
devices = [] devices = []
for query in device_set.strip(';').split(';'): # Split regexes on semicolons for query in device_set.split(';'): # Split regexes on semicolons
devices += Device.objects.filter(name__regex=query).select_related('device_role') devices += Device.objects.filter(name__regex=query).select_related('device_role')
# Remove duplicate devices
devices = [d for d in devices if d.id not in seen]
seen.update([d.id for d in devices])
for d in devices: for d in devices:
bg_color = '#{}'.format(d.device_role.color) bg_color = '#{}'.format(d.device_role.color)
fg_color = '#{}'.format(foreground_color(d.device_role.color)) fg_color = '#{}'.format(foreground_color(d.device_role.color))
@@ -394,24 +388,6 @@ class ImageAttachment(models.Model):
return None return None
#
# Report results
#
class ReportResult(models.Model):
"""
This model stores the results from running a user-defined report.
"""
report = models.CharField(max_length=255, unique=True)
created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='+', blank=True, null=True)
failed = models.BooleanField()
data = JSONField()
class Meta:
ordering = ['report']
# #
# User actions # User actions
# #

View File

@@ -1,189 +0,0 @@
from __future__ import unicode_literals
import importlib
import inspect
import pkgutil
from collections import OrderedDict
from django.conf import settings
from django.utils import timezone
from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_LEVEL_CODES, LOG_SUCCESS, LOG_WARNING
from .models import ReportResult
def is_report(obj):
"""
Returns True if the given object is a Report.
"""
if obj in Report.__subclasses__():
return True
return False
def get_report(module_name, report_name):
"""
Return a specific report from within a module.
"""
module = importlib.import_module('reports.{}'.format(module_name))
report = getattr(module, report_name, None)
if report is None:
return None
return report()
def get_reports():
"""
Compile a list of all reports available across all modules in the reports path. Returns a list of tuples:
[
(module_name, (report, report, report, ...)),
(module_name, (report, report, report, ...)),
...
]
"""
module_list = []
# Iterate through all modules within the reports path. These are the user-created files in which reports are
# defined.
for importer, module_name, is_pkg in pkgutil.walk_packages([settings.REPORTS_ROOT]):
module = importlib.import_module('reports.{}'.format(module_name))
report_list = [cls() for _, cls in inspect.getmembers(module, is_report)]
module_list.append((module_name, report_list))
return module_list
class Report(object):
"""
NetBox users can extend this object to write custom reports to be used for validating data within NetBox. Each
report must have one or more test methods named `test_*`.
The `_results` attribute of a completed report will take the following form:
{
'test_bar': {
'failures': 42,
'log': [
(<datetime>, <level>, <object>, <message>),
...
]
},
'test_foo': {
'failures': 0,
'log': [
(<datetime>, <level>, <object>, <message>),
...
]
}
}
"""
description = None
def __init__(self):
self._results = OrderedDict()
self.active_test = None
self.failed = False
# Compile test methods and initialize results skeleton
test_methods = []
for method in dir(self):
if method.startswith('test_') and callable(getattr(self, method)):
test_methods.append(method)
self._results[method] = OrderedDict([
('success', 0),
('info', 0),
('warning', 0),
('failure', 0),
('log', []),
])
if not test_methods:
raise Exception("A report must contain at least one test method.")
self.test_methods = test_methods
@property
def module(self):
return self.__module__.rsplit('.', 1)[1]
@property
def name(self):
return self.__class__.__name__
@property
def full_name(self):
return '.'.join([self.module, self.name])
def _log(self, obj, message, level=LOG_DEFAULT):
"""
Log a message from a test method. Do not call this method directly; use one of the log_* wrappers below.
"""
if level not in LOG_LEVEL_CODES:
raise Exception("Unknown logging level: {}".format(level))
self._results[self.active_test]['log'].append((
timezone.now().isoformat(),
LOG_LEVEL_CODES.get(level),
str(obj) if obj else None,
obj.get_absolute_url() if getattr(obj, 'get_absolute_url', None) else None,
message,
))
def log(self, message):
"""
Log a message which is not associated with a particular object.
"""
self._log(None, message, level=LOG_DEFAULT)
def log_success(self, obj, message=None):
"""
Record a successful test against an object. Logging a message is optional.
"""
if message:
self._log(obj, message, level=LOG_SUCCESS)
self._results[self.active_test]['success'] += 1
def log_info(self, obj, message):
"""
Log an informational message.
"""
self._log(obj, message, level=LOG_INFO)
self._results[self.active_test]['info'] += 1
def log_warning(self, obj, message):
"""
Log a warning.
"""
self._log(obj, message, level=LOG_WARNING)
self._results[self.active_test]['warning'] += 1
def log_failure(self, obj, message):
"""
Log a failure. Calling this method will automatically mark the report as failed.
"""
self._log(obj, message, level=LOG_FAILURE)
self._results[self.active_test]['failure'] += 1
self.failed = True
def run(self):
"""
Run the report and return its results. Each test method will be executed in order.
"""
for method_name in self.test_methods:
self.active_test = method_name
test_method = getattr(self, method_name)
test_method()
# Delete any previous ReportResult and create a new one to record the result.
ReportResult.objects.filter(report=self.full_name).delete()
result = ReportResult(report=self.full_name, failed=self.failed, data=self._results)
result.save()
self.result = result
# Perform any post-run tasks
self.post_run()
def post_run(self):
"""
Extend this method to include any tasks which should execute after the report has been run.
"""
pass

View File

@@ -1,11 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re import re
import time import time
from ncclient import manager
import paramiko import paramiko
import xmltodict import xmltodict
from ncclient import manager
CONNECT_TIMEOUT = 5 # seconds CONNECT_TIMEOUT = 5 # seconds
@@ -20,6 +20,19 @@ class RPCClient(object):
except AttributeError: except AttributeError:
raise Exception("Specified device ({}) does not have a primary IP defined.".format(device)) raise Exception("Specified device ({}) does not have a primary IP defined.".format(device))
def get_lldp_neighbors(self):
"""
Returns a list of dictionaries, each representing an LLDP neighbor adjacency.
{
'local-interface': <str>,
'name': <str>,
'remote-interface': <str>,
'chassis-id': <str>,
}
"""
raise NotImplementedError("Feature not implemented for this platform.")
def get_inventory(self): def get_inventory(self):
""" """
Returns a dictionary representing the device chassis and installed inventory items. Returns a dictionary representing the device chassis and installed inventory items.
@@ -110,6 +123,30 @@ class JunosNC(RPCClient):
# Close the connection to the device # Close the connection to the device
self.manager.close_session() self.manager.close_session()
def get_lldp_neighbors(self):
rpc_reply = self.manager.dispatch('get-lldp-neighbors-information')
lldp_neighbors_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['lldp-neighbors-information']['lldp-neighbor-information']
result = []
for neighbor_raw in lldp_neighbors_raw:
neighbor = dict()
neighbor['local-interface'] = neighbor_raw.get('lldp-local-port-id')
name = neighbor_raw.get('lldp-remote-system-name')
if name:
neighbor['name'] = name.split('.')[0] # Split hostname from domain if one is present
else:
neighbor['name'] = ''
try:
neighbor['remote-interface'] = neighbor_raw['lldp-remote-port-description']
except KeyError:
# Older versions of Junos report on interface ID instead of description
neighbor['remote-interface'] = neighbor_raw.get('lldp-remote-port-id')
neighbor['chassis-id'] = neighbor_raw.get('lldp-remote-chassis-id')
result.append(neighbor)
return result
def get_inventory(self): def get_inventory(self):
def glean_items(node, depth=0): def glean_items(node, depth=0):

View File

@@ -1,14 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.models import Device from dcim.models import Device
from extras.constants import GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_SITE, ExportTemplate
from extras.models import Graph, ExportTemplate
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@@ -1,17 +1,19 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import date from datetime import date
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.models import Site from dcim.models import Site
from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL from extras.models import (
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
CF_TYPE_SELECT, CF_TYPE_URL,
)
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@@ -4,6 +4,7 @@ from django.conf.urls import url
from extras import views from extras import views
app_name = 'extras' app_name = 'extras'
urlpatterns = [ urlpatterns = [
@@ -11,9 +12,4 @@ urlpatterns = [
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'), url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
# Reports
url(r'^reports/$', views.ReportListView.as_view(), name='report_list'),
url(r'^reports/(?P<name>[^/]+\.[^/]+)/$', views.ReportView.as_view(), name='report'),
url(r'^reports/(?P<name>[^/]+\.[^/]+)/run/$', views.ReportRunView.as_view(), name='report_run'),
] ]

View File

@@ -1,27 +1,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.http import Http404 from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.safestring import mark_safe
from django.views.generic import View
from utilities.forms import ConfirmationForm
from utilities.views import ObjectDeleteView, ObjectEditView from utilities.views import ObjectDeleteView, ObjectEditView
from .forms import ImageAttachmentForm from .forms import ImageAttachmentForm
from .models import ImageAttachment, ReportResult, UserAction from .models import ImageAttachment
from .reports import get_report, get_reports
#
# Image attachments
#
class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView): class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'extras.change_imageattachment' permission_required = 'extras.change_imageattachment'
model = ImageAttachment model = ImageAttachment
model_form = ImageAttachmentForm form_class = ImageAttachmentForm
def alter_obj(self, imageattachment, request, args, kwargs): def alter_obj(self, imageattachment, request, args, kwargs):
if not imageattachment.pk: if not imageattachment.pk:
@@ -40,79 +30,3 @@ class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
def get_return_url(self, request, imageattachment): def get_return_url(self, request, imageattachment):
return imageattachment.parent.get_absolute_url() return imageattachment.parent.get_absolute_url()
#
# Reports
#
class ReportListView(View):
"""
Retrieve all of the available reports from disk and the recorded ReportResult (if any) for each.
"""
def get(self, request):
reports = get_reports()
results = {r.report: r for r in ReportResult.objects.all()}
ret = []
for module, report_list in reports:
module_reports = []
for report in report_list:
report.result = results.get(report.full_name, None)
module_reports.append(report)
ret.append((module, module_reports))
return render(request, 'extras/report_list.html', {
'reports': ret,
})
class ReportView(View):
"""
Display a single Report and its associated ReportResult (if any).
"""
def get(self, request, name):
# Retrieve the Report by "<module>.<report>"
module_name, report_name = name.split('.')
report = get_report(module_name, report_name)
if report is None:
raise Http404
# Attach the ReportResult (if any)
report.result = ReportResult.objects.filter(report=report.full_name).first()
return render(request, 'extras/report.html', {
'report': report,
'run_form': ConfirmationForm(),
})
class ReportRunView(PermissionRequiredMixin, View):
"""
Run a Report and record a new ReportResult.
"""
permission_required = 'extras.add_reportresult'
def post(self, request, name):
# Retrieve the Report by "<module>.<report>"
module_name, report_name = name.split('.')
report = get_report(module_name, report_name)
if report is None:
raise Http404
form = ConfirmationForm(request.POST)
if form.is_valid():
# Run the Report. A new ReportResult is created.
report.run()
result = 'failed' if report.failed else 'passed'
msg = "Ran report {} ({})".format(report.full_name, result)
messages.success(request, mark_safe(msg))
UserAction.objects.log_create(request.user, report.result, msg)
return redirect('extras:report', name=report.full_name)

View File

@@ -1,5 +1,4 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
@@ -7,13 +6,12 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.constants import ( from ipam.models import (
IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES, Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
) )
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
from virtualization.api.serializers import NestedVirtualMachineSerializer
# #
@@ -47,7 +45,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
# Roles # Roles
# #
class RoleSerializer(ValidatedModelSerializer): class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Role model = Role
@@ -66,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
# RIRs # RIRs
# #
class RIRSerializer(ValidatedModelSerializer): class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RIR model = RIR
@@ -241,22 +239,12 @@ class WritablePrefixSerializer(CustomFieldModelSerializer):
# IP addresses # IP addresses
# #
class IPAddressInterfaceSerializer(InterfaceSerializer):
virtual_machine = NestedVirtualMachineSerializer()
class Meta(InterfaceSerializer.Meta):
fields = [
'id', 'device', 'virtual_machine', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address',
'mgmt_only', 'description', 'is_connected', 'interface_connection', 'circuit_termination',
]
class IPAddressSerializer(CustomFieldModelSerializer): class IPAddressSerializer(CustomFieldModelSerializer):
vrf = NestedVRFSerializer() vrf = NestedVRFSerializer()
tenant = NestedTenantSerializer() tenant = NestedTenantSerializer()
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES) status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES) role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
interface = IPAddressInterfaceSerializer() interface = InterfaceSerializer()
class Meta: class Meta:
model = IPAddress model = IPAddress
@@ -273,7 +261,6 @@ class NestedIPAddressSerializer(serializers.ModelSerializer):
model = IPAddress model = IPAddress
fields = ['id', 'url', 'family', 'address'] fields = ['id', 'url', 'family', 'address']
IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer() IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer()
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer() IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer()
@@ -308,18 +295,17 @@ class AvailableIPSerializer(serializers.Serializer):
class ServiceSerializer(serializers.ModelSerializer): class ServiceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
virtual_machine = NestedVirtualMachineSerializer()
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES) protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
ipaddresses = NestedIPAddressSerializer(many=True) ipaddresses = NestedIPAddressSerializer(many=True)
class Meta: class Meta:
model = Service model = Service
fields = ['id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description'] fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError. # TODO: Figure out how to use ModelValidationMixin with ManyToManyFields. Calling clean() yields a ValueError.
class WritableServiceSerializer(serializers.ModelSerializer): class WritableServiceSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Service model = Service
fields = ['id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description'] fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']

View File

@@ -16,9 +16,6 @@ class IPAMRootView(routers.APIRootView):
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.APIRootView = IPAMRootView router.APIRootView = IPAMRootView
# Field choices
router.register(r'_choices', views.IPAMFieldChoicesViewSet, base_name='field-choice')
# VRFs # VRFs
router.register(r'vrfs', views.VRFViewSet) router.register(r'vrfs', views.VRFViewSet)

View File

@@ -1,34 +1,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings
from django.shortcuts import get_object_or_404
from rest_framework import status from rest_framework import status
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from extras.api.views import CustomFieldModelViewSet from django.conf import settings
from ipam import filters from django.shortcuts import get_object_or_404
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin from ipam import filters
from extras.api.views import CustomFieldModelViewSet
from utilities.api import WritableSerializerMixin
from . import serializers from . import serializers
#
# Field choices
#
class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Aggregate, ['family']),
(Prefix, ['family', 'status']),
(IPAddress, ['family', 'status', 'role']),
(VLAN, ['status']),
(Service, ['protocol']),
)
# #
# VRFs # VRFs
# #
@@ -111,7 +98,7 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
# Create the new IP address # Create the new IP address
data = request.data.copy() data = request.data.copy()
data['address'] = '{}/{}'.format(ipaddress, prefix.prefix.prefixlen) data['address'] = '{}/{}'.format(ipaddress, prefix.prefix.prefixlen)
data['vrf'] = prefix.vrf.pk if prefix.vrf else None data['vrf'] = prefix.vrf
serializer = serializers.WritableIPAddressSerializer(data=data) serializer = serializers.WritableIPAddressSerializer(data=data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@@ -128,11 +115,7 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
limit = min(limit, settings.MAX_PAGE_SIZE) limit = min(limit, settings.MAX_PAGE_SIZE)
# Calculate available IPs within the prefix # Calculate available IPs within the prefix
ip_list = [] ip_list = list(prefix.get_available_ips())[:limit]
for index, ip in enumerate(prefix.get_available_ips(), start=1):
ip_list.append(ip)
if index == limit:
break
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={ serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
'request': request, 'request': request,
'prefix': prefix.prefix, 'prefix': prefix.prefix,
@@ -147,11 +130,7 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
# #
class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet): class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = IPAddress.objects.select_related( queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
'vrf__tenant', 'tenant', 'nat_inside'
).prefetch_related(
'interface__device', 'interface__virtual_machine'
)
serializer_class = serializers.IPAddressSerializer serializer_class = serializers.IPAddressSerializer
write_serializer_class = serializers.WritableIPAddressSerializer write_serializer_class = serializers.WritableIPAddressSerializer
filter_class = filters.IPAddressFilter filter_class = filters.IPAddressFilter

View File

@@ -39,7 +39,6 @@ IPADDRESS_ROLE_VIP = 40
IPADDRESS_ROLE_VRRP = 41 IPADDRESS_ROLE_VRRP = 41
IPADDRESS_ROLE_HSRP = 42 IPADDRESS_ROLE_HSRP = 42
IPADDRESS_ROLE_GLBP = 43 IPADDRESS_ROLE_GLBP = 43
IPADDRESS_ROLE_CARP = 44
IPADDRESS_ROLE_CHOICES = ( IPADDRESS_ROLE_CHOICES = (
(IPADDRESS_ROLE_LOOPBACK, 'Loopback'), (IPADDRESS_ROLE_LOOPBACK, 'Loopback'),
(IPADDRESS_ROLE_SECONDARY, 'Secondary'), (IPADDRESS_ROLE_SECONDARY, 'Secondary'),
@@ -48,7 +47,6 @@ IPADDRESS_ROLE_CHOICES = (
(IPADDRESS_ROLE_VRRP, 'VRRP'), (IPADDRESS_ROLE_VRRP, 'VRRP'),
(IPADDRESS_ROLE_HSRP, 'HSRP'), (IPADDRESS_ROLE_HSRP, 'HSRP'),
(IPADDRESS_ROLE_GLBP, 'GLBP'), (IPADDRESS_ROLE_GLBP, 'GLBP'),
(IPADDRESS_ROLE_CARP, 'CARP'),
) )
# VLAN statuses # VLAN statuses
@@ -61,7 +59,7 @@ VLAN_STATUS_CHOICES = (
(VLAN_STATUS_DEPRECATED, 'Deprecated') (VLAN_STATUS_DEPRECATED, 'Deprecated')
) )
# Bootstrap CSS classes # Bootstrap CSS classes for various statuses
STATUS_CHOICE_CLASSES = { STATUS_CHOICE_CLASSES = {
0: 'default', 0: 'default',
1: 'primary', 1: 'primary',
@@ -70,16 +68,6 @@ STATUS_CHOICE_CLASSES = {
4: 'warning', 4: 'warning',
5: 'success', 5: 'success',
} }
ROLE_CHOICE_CLASSES = {
10: 'default',
20: 'primary',
30: 'warning',
40: 'success',
41: 'success',
42: 'success',
43: 'success',
44: 'success',
}
# IP protocols (for services) # IP protocols (for services)
IP_PROTOCOL_TCP = 6 IP_PROTOCOL_TCP = 6

View File

@@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from netaddr import IPNetwork
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from netaddr import IPNetwork
from .formfields import IPFormField from .formfields import IPFormField
from .lookups import ( from .lookups import (

View File

@@ -1,17 +1,19 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q
from netaddr import IPNetwork from netaddr import IPNetwork
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from django.db.models import Q
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NumericInFilter from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from virtualization.models import VirtualMachine from .models import (
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -20,12 +22,12 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = django_filters.ModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant__slug', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
@@ -99,54 +101,45 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
# TODO: Deprecate in v2.3.0
parent = django_filters.CharFilter( parent = django_filters.CharFilter(
method='search_within_include', method='search_by_parent',
label='Parent prefix (deprecated)', label='Parent prefix',
)
within = django_filters.CharFilter(
method='search_within',
label='Within prefix',
)
within_include = django_filters.CharFilter(
method='search_within_include',
label='Within and including prefix',
) )
mask_length = django_filters.NumberFilter( mask_length = django_filters.NumberFilter(
method='filter_mask_length', method='filter_mask_length',
label='Mask length', label='Mask length',
) )
vrf_id = django_filters.ModelMultipleChoiceFilter( vrf_id = NullableModelMultipleChoiceFilter(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
label='VRF', label='VRF',
) )
vrf = django_filters.ModelMultipleChoiceFilter( vrf = NullableModelMultipleChoiceFilter(
name='vrf__rd', name='vrf',
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='rd', to_field_name='rd',
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = django_filters.ModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant__slug', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = NullableModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
site = django_filters.ModelMultipleChoiceFilter( site = NullableModelMultipleChoiceFilter(
name='site__slug', name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
vlan_id = django_filters.ModelMultipleChoiceFilter( vlan_id = NullableModelMultipleChoiceFilter(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
label='VLAN (ID)', label='VLAN (ID)',
) )
@@ -154,19 +147,18 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
name='vlan__vid', name='vlan__vid',
label='VLAN number (1-4095)', label='VLAN number (1-4095)',
) )
role_id = django_filters.ModelMultipleChoiceFilter( role_id = NullableModelMultipleChoiceFilter(
queryset=Role.objects.all(), queryset=Role.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
role = django_filters.ModelMultipleChoiceFilter( role = NullableModelMultipleChoiceFilter(
name='role__slug', name='role',
queryset=Role.objects.all(), queryset=Role.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=PREFIX_STATUS_CHOICES, choices=PREFIX_STATUS_CHOICES
null_value=None
) )
class Meta: class Meta:
@@ -184,17 +176,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
pass pass
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def search_within(self, queryset, name, value): def search_by_parent(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
query = str(IPNetwork(value).cidr)
return queryset.filter(prefix__net_contained=query)
except (AddrFormatError, ValueError):
return queryset.none()
def search_within_include(self, queryset, name, value):
value = value.strip() value = value.strip()
if not value: if not value:
return queryset return queryset
@@ -224,22 +206,22 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='filter_mask_length', method='filter_mask_length',
label='Mask length', label='Mask length',
) )
vrf_id = django_filters.ModelMultipleChoiceFilter( vrf_id = NullableModelMultipleChoiceFilter(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
label='VRF', label='VRF',
) )
vrf = django_filters.ModelMultipleChoiceFilter( vrf = NullableModelMultipleChoiceFilter(
name='vrf__rd', name='vrf',
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='rd', to_field_name='rd',
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = django_filters.ModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant__slug', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
@@ -255,24 +237,12 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='name', to_field_name='name',
label='Device (name)', label='Device (name)',
) )
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
name='interface__virtual_machine',
queryset=VirtualMachine.objects.all(),
label='Virtual machine (ID)',
)
virtual_machine = django_filters.ModelMultipleChoiceFilter(
name='interface__virtual_machine__name',
queryset=VirtualMachine.objects.all(),
to_field_name='name',
label='Virtual machine (name)',
)
interface_id = django_filters.ModelMultipleChoiceFilter( interface_id = django_filters.ModelMultipleChoiceFilter(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Interface (ID)', label='Interface (ID)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES, choices=IPADDRESS_STATUS_CHOICES
null_value=None
) )
role = django_filters.MultipleChoiceFilter( role = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_ROLE_CHOICES choices=IPADDRESS_ROLE_CHOICES
@@ -285,10 +255,12 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
qs_filter = ( qs_filter = Q(description__icontains=value)
Q(description__icontains=value) | try:
Q(address__istartswith=value) ipaddress = str(IPNetwork(value.strip()))
) qs_filter |= Q(address__net_host=ipaddress)
except (AddrFormatError, ValueError):
pass
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def search_by_parent(self, queryset, name, value): def search_by_parent(self, queryset, name, value):
@@ -308,12 +280,12 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
class VLANGroupFilter(django_filters.FilterSet): class VLANGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter( site_id = NullableModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
site = django_filters.ModelMultipleChoiceFilter( site = NullableModelMultipleChoiceFilter(
name='site__slug', name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
@@ -330,49 +302,48 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = NullableModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
site = django_filters.ModelMultipleChoiceFilter( site = NullableModelMultipleChoiceFilter(
name='site__slug', name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = django_filters.ModelMultipleChoiceFilter( group_id = NullableModelMultipleChoiceFilter(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
group = django_filters.ModelMultipleChoiceFilter( group = NullableModelMultipleChoiceFilter(
name='group__slug', name='group',
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = django_filters.ModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant__slug', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
role_id = django_filters.ModelMultipleChoiceFilter( role_id = NullableModelMultipleChoiceFilter(
queryset=Role.objects.all(), queryset=Role.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
role = django_filters.ModelMultipleChoiceFilter( role = NullableModelMultipleChoiceFilter(
name='role__slug', name='role',
queryset=Role.objects.all(), queryset=Role.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=VLAN_STATUS_CHOICES, choices=VLAN_STATUS_CHOICES
null_value=None
) )
class Meta: class Meta:
@@ -401,16 +372,6 @@ class ServiceFilter(django_filters.FilterSet):
to_field_name='name', to_field_name='name',
label='Device (name)', label='Device (name)',
) )
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualMachine.objects.all(),
label='Virtual machine (ID)',
)
virtual_machine = django_filters.ModelMultipleChoiceFilter(
name='virtual_machine__name',
queryset=VirtualMachine.objects.all(),
to_field_name='name',
label='Virtual machine (name)',
)
class Meta: class Meta:
model = Service model = Service

View File

@@ -70,7 +70,7 @@
"family": 4, "family": 4,
"address": "10.0.255.1/32", "address": "10.0.255.1/32",
"vrf": null, "vrf": null,
"interface_id": 3, "interface": 3,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -84,7 +84,7 @@
"family": 4, "family": 4,
"address": "169.254.254.1/31", "address": "169.254.254.1/31",
"vrf": null, "vrf": null,
"interface_id": 4, "interface": 4,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -98,7 +98,7 @@
"family": 4, "family": 4,
"address": "10.0.255.2/32", "address": "10.0.255.2/32",
"vrf": null, "vrf": null,
"interface_id": 185, "interface": 185,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -112,7 +112,7 @@
"family": 4, "family": 4,
"address": "169.254.1.1/31", "address": "169.254.1.1/31",
"vrf": null, "vrf": null,
"interface_id": 213, "interface": 213,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -126,7 +126,7 @@
"family": 4, "family": 4,
"address": "10.0.254.1/24", "address": "10.0.254.1/24",
"vrf": null, "vrf": null,
"interface_id": 12, "interface": 12,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -140,7 +140,7 @@
"family": 4, "family": 4,
"address": "10.15.21.1/31", "address": "10.15.21.1/31",
"vrf": null, "vrf": null,
"interface_id": 218, "interface": 218,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -154,7 +154,7 @@
"family": 4, "family": 4,
"address": "10.15.21.2/31", "address": "10.15.21.2/31",
"vrf": null, "vrf": null,
"interface_id": 9, "interface": 9,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -168,7 +168,7 @@
"family": 4, "family": 4,
"address": "10.15.22.1/31", "address": "10.15.22.1/31",
"vrf": null, "vrf": null,
"interface_id": 8, "interface": 8,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -182,7 +182,7 @@
"family": 4, "family": 4,
"address": "10.15.20.1/31", "address": "10.15.20.1/31",
"vrf": null, "vrf": null,
"interface_id": 7, "interface": 7,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -196,7 +196,7 @@
"family": 4, "family": 4,
"address": "10.16.20.1/31", "address": "10.16.20.1/31",
"vrf": null, "vrf": null,
"interface_id": 216, "interface": 216,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -210,7 +210,7 @@
"family": 4, "family": 4,
"address": "10.15.22.2/31", "address": "10.15.22.2/31",
"vrf": null, "vrf": null,
"interface_id": 206, "interface": 206,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -224,7 +224,7 @@
"family": 4, "family": 4,
"address": "10.16.22.1/31", "address": "10.16.22.1/31",
"vrf": null, "vrf": null,
"interface_id": 217, "interface": 217,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -238,7 +238,7 @@
"family": 4, "family": 4,
"address": "10.16.22.2/31", "address": "10.16.22.2/31",
"vrf": null, "vrf": null,
"interface_id": 205, "interface": 205,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -252,7 +252,7 @@
"family": 4, "family": 4,
"address": "10.16.20.2/31", "address": "10.16.20.2/31",
"vrf": null, "vrf": null,
"interface_id": 211, "interface": 211,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -266,7 +266,7 @@
"family": 4, "family": 4,
"address": "10.15.22.2/31", "address": "10.15.22.2/31",
"vrf": null, "vrf": null,
"interface_id": 212, "interface": 212,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -280,7 +280,7 @@
"family": 4, "family": 4,
"address": "10.0.254.2/32", "address": "10.0.254.2/32",
"vrf": null, "vrf": null,
"interface_id": 188, "interface": 188,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -294,7 +294,7 @@
"family": 4, "family": 4,
"address": "169.254.1.1/31", "address": "169.254.1.1/31",
"vrf": null, "vrf": null,
"interface_id": 200, "interface": 200,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }
@@ -308,7 +308,7 @@
"family": 4, "family": 4,
"address": "169.254.1.2/31", "address": "169.254.1.2/31",
"vrf": null, "vrf": null,
"interface_id": 194, "interface": 194,
"nat_inside": null, "nat_inside": null,
"description": "" "description": ""
} }

View File

@@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from netaddr import IPNetwork, AddrFormatError
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from netaddr import IPNetwork, AddrFormatError
# #

View File

@@ -13,9 +13,11 @@ from utilities.forms import (
ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, SlugField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, SlugField,
add_blank_choice, add_blank_choice,
) )
from virtualization.models import VirtualMachine from .models import (
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
)
IP_FAMILY_CHOICES = [ IP_FAMILY_CHOICES = [
('', 'All'), ('', 'All'),
@@ -23,8 +25,11 @@ IP_FAMILY_CHOICES = [
(6, 'IPv6'), (6, 'IPv6'),
] ]
PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 128)]) PREFIX_MASK_LENGTH_CHOICES = [
IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)]) ('', '---------'),
] + [(i, i) for i in range(1, 128)]
IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
# #
@@ -94,17 +99,6 @@ class RIRForm(BootstrapMixin, forms.ModelForm):
fields = ['name', 'slug', 'is_private'] fields = ['name', 'slug', 'is_private']
class RIRCSVForm(forms.ModelForm):
slug = SlugField()
class Meta:
model = RIR
fields = ['name', 'slug', 'is_private']
help_texts = {
'name': 'RIR name',
}
class RIRFilterForm(BootstrapMixin, forms.Form): class RIRFilterForm(BootstrapMixin, forms.Form):
is_private = forms.NullBooleanField(required=False, label='Private', widget=forms.Select(choices=[ is_private = forms.NullBooleanField(required=False, label='Private', widget=forms.Select(choices=[
('', '---------'), ('', '---------'),
@@ -177,17 +171,6 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
fields = ['name', 'slug'] fields = ['name', 'slug']
class RoleCSVForm(forms.ModelForm):
slug = SlugField()
class Meta:
model = Role
fields = ['name', 'slug']
help_texts = {
'name': 'Role name',
}
# #
# Prefixes # Prefixes
# #
@@ -234,7 +217,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
# Initialize helper selectors # Initialize helper selectors
instance = kwargs.get('instance') instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy() initial = kwargs.get('initial', {})
if instance and instance.vlan is not None: if instance and instance.vlan is not None:
initial['vlan_group'] = instance.vlan.group initial['vlan_group'] = instance.vlan.group
kwargs['initial'] = initial kwargs['initial'] = initial
@@ -281,7 +264,7 @@ class PrefixCSVForm(forms.ModelForm):
required=False required=False
) )
status = CSVChoiceField( status = CSVChoiceField(
choices=PREFIX_STATUS_CHOICES, choices=IPADDRESS_STATUS_CHOICES,
help_text='Operational status' help_text='Operational status'
) )
role = forms.ModelChoiceField( role = forms.ModelChoiceField(
@@ -359,7 +342,7 @@ def prefix_status_choices():
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Prefix model = Prefix
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')
within_include = forms.CharField(required=False, label='Search within', widget=forms.TextInput(attrs={ parent = forms.CharField(required=False, label='Parent prefix', widget=forms.TextInput(attrs={
'placeholder': 'Prefix', 'placeholder': 'Prefix',
})) }))
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family') family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
@@ -394,9 +377,50 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
# #
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm): class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm):
interface = forms.ModelChoiceField( interface_site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
label='Site',
widget=forms.Select(
attrs={'filter-for': 'interface_rack'}
)
)
interface_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'interface_site'),
),
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{interface_site}}',
display_field='display_name',
attrs={'filter-for': 'interface_device', 'nullable': 'true'}
)
)
interface_device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'interface_site'),
('rack', 'interface_rack'),
),
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}',
display_field='display_name',
attrs={'filter-for': 'interface'}
)
)
interface = ChainedModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False chains=(
('device', 'interface_device'),
),
required=False,
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
)
) )
nat_site = forms.ModelChoiceField( nat_site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
@@ -455,20 +479,24 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
obj_label='address' obj_label='address'
) )
) )
primary_for_parent = forms.BooleanField(required=False, label='Make this the primary IP for the device/VM') primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device')
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site', 'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'nat_inside', 'tenant_group', 'tenant',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Initialize helper selectors # Initialize helper selectors
instance = kwargs.get('instance') instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy() initial = kwargs.get('initial', {})
if instance and instance.interface is not None:
initial['interface_site'] = instance.interface.device.site
initial['interface_rack'] = instance.interface.device.rack
initial['interface_device'] = instance.interface.device
if instance and instance.nat_inside and instance.nat_inside.device is not None: if instance and instance.nat_inside and instance.nat_inside.device is not None:
initial['nat_site'] = instance.nat_inside.device.site initial['nat_site'] = instance.nat_inside.device.site
initial['nat_rack'] = instance.nat_inside.device.rack initial['nat_rack'] = instance.nat_inside.device.rack
@@ -479,30 +507,22 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
self.fields['vrf'].empty_label = 'Global' self.fields['vrf'].empty_label = 'Global'
# Limit interface selections to those belonging to the parent device/VM # Initialize primary_for_device if IP address is already assigned
if self.instance and self.instance.interface: if self.instance.interface is not None:
self.fields['interface'].queryset = Interface.objects.filter( device = self.instance.interface.device
device=self.instance.interface.device, virtual_machine=self.instance.interface.virtual_machine
)
else:
self.fields['interface'].choices = []
# Initialize primary_for_parent if IP address is already assigned
if self.instance.pk and self.instance.interface is not None:
parent = self.instance.interface.parent
if ( if (
self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or self.instance.address.version == 4 and device.primary_ip4 == self.instance or
self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk self.instance.address.version == 6 and device.primary_ip6 == self.instance
): ):
self.initial['primary_for_parent'] = True self.initial['primary_for_device'] = True
def clean(self): def clean(self):
super(IPAddressForm, self).clean() super(IPAddressForm, self).clean()
# Primary IP assignment is only available if an interface has been assigned. # Primary IP assignment is only available if an interface has been assigned.
if self.cleaned_data.get('primary_for_parent') and not self.cleaned_data.get('interface'): if self.cleaned_data.get('primary_for_device') and not self.cleaned_data.get('interface'):
self.add_error( self.add_error(
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs." 'primary_for_device', "Only IP addresses assigned to an interface can be designated as primary IPs."
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@@ -510,13 +530,13 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
ipaddress = super(IPAddressForm, self).save(*args, **kwargs) ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
# Assign this IPAddress as the primary for the associated Device. # Assign this IPAddress as the primary for the associated Device.
if self.cleaned_data['primary_for_parent']: if self.cleaned_data['primary_for_device']:
parent = self.cleaned_data['interface'].parent device = self.cleaned_data['interface'].device
if ipaddress.address.version == 4: if ipaddress.address.version == 4:
parent.primary_ip4 = ipaddress device.primary_ip4 = ipaddress
else: else:
parent.primary_ip6 = ipaddress device.primary_ip6 = ipaddress
parent.save() device.save()
# Clear assignment as primary for device if set. # Clear assignment as primary for device if set.
else: else:
@@ -534,7 +554,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
return ipaddress return ipaddress
class IPAddressBulkCreateForm(BootstrapMixin, forms.Form): class IPAddressPatternForm(BootstrapMixin, forms.Form):
pattern = ExpandableIPAddressField(label='Address pattern') pattern = ExpandableIPAddressField(label='Address pattern')
@@ -586,15 +606,6 @@ class IPAddressCSVForm(forms.ModelForm):
'invalid_choice': 'Device not found.', 'invalid_choice': 'Device not found.',
} }
) )
virtual_machine = forms.ModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned virtual machine',
error_messages={
'invalid_choice': 'Virtual machine not found.',
}
)
interface_name = forms.CharField( interface_name = forms.CharField(
help_text='Name of assigned interface', help_text='Name of assigned interface',
required=False required=False
@@ -606,47 +617,30 @@ class IPAddressCSVForm(forms.ModelForm):
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = ['address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description']
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
'description',
]
def clean(self): def clean(self):
super(IPAddressCSVForm, self).clean() super(IPAddressCSVForm, self).clean()
device = self.cleaned_data.get('device') device = self.cleaned_data.get('device')
virtual_machine = self.cleaned_data.get('virtual_machine')
interface_name = self.cleaned_data.get('interface_name') interface_name = self.cleaned_data.get('interface_name')
is_primary = self.cleaned_data.get('is_primary') is_primary = self.cleaned_data.get('is_primary')
# Validate interface # Validate interface
if interface_name and device: if device and interface_name:
try: try:
self.instance.interface = Interface.objects.get(device=device, name=interface_name) self.instance.interface = Interface.objects.get(device=device, name=interface_name)
except Interface.DoesNotExist: except Interface.DoesNotExist:
raise forms.ValidationError("Invalid interface {} for device {}".format( raise forms.ValidationError("Invalid interface {} for device {}".format(interface_name, device))
interface_name, device elif device and not interface_name:
)) raise forms.ValidationError("Device set ({}) but interface missing".format(device))
elif interface_name and virtual_machine: elif interface_name and not device:
try: raise forms.ValidationError("Interface set ({}) but device missing or invalid".format(interface_name))
self.instance.interface = Interface.objects.get(virtual_machine=virtual_machine, name=interface_name)
except Interface.DoesNotExist:
raise forms.ValidationError("Invalid interface {} for virtual machine {}".format(
interface_name, virtual_machine
))
elif interface_name:
raise forms.ValidationError("Interface given ({}) but parent device/virtual machine not specified".format(
interface_name
))
elif device:
raise forms.ValidationError("Device specified ({}) but interface missing".format(device))
elif virtual_machine:
raise forms.ValidationError("Virtual machine specified ({}) but interface missing".format(virtual_machine))
# Validate is_primary # Validate is_primary
if is_primary and not device and not virtual_machine: if is_primary and not device:
raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP") raise forms.ValidationError("No device specified; cannot set as primary IP")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@@ -656,22 +650,17 @@ class IPAddressCSVForm(forms.ModelForm):
device=self.cleaned_data['device'], device=self.cleaned_data['device'],
name=self.cleaned_data['interface_name'] name=self.cleaned_data['interface_name']
) )
elif self.cleaned_data['virtual_machine'] and self.cleaned_data['interface_name']:
self.instance.interface = Interface.objects.get(
virtual_machine=self.cleaned_data['virtual_machine'],
name=self.cleaned_data['interface_name']
)
ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs) ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs)
# Set as primary for device/VM # Set as primary for device
if self.cleaned_data['is_primary']: if self.cleaned_data['is_primary']:
parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine'] device = self.cleaned_data['device']
if self.instance.address.version == 4: if self.instance.address.version == 4:
parent.primary_ip4 = ipaddress device.primary_ip4 = ipaddress
elif self.instance.address.version == 6: elif self.instance.address.version == 6:
parent.primary_ip6 = ipaddress device.primary_ip6 = ipaddress
parent.save() device.save()
return ipaddress return ipaddress
@@ -688,11 +677,6 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
nullable_fields = ['vrf', 'role', 'tenant', 'description'] nullable_fields = ['vrf', 'role', 'tenant', 'description']
class IPAddressAssignForm(BootstrapMixin, forms.Form):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
address = forms.CharField(label='IP Address')
def ipaddress_status_choices(): def ipaddress_status_choices():
status_counts = {} status_counts = {}
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
@@ -742,26 +726,6 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
fields = ['site', 'name', 'slug'] fields = ['site', 'name', 'slug']
class VLANGroupCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Site not found.',
}
)
slug = SlugField()
class Meta:
model = VLANGroup
fields = ['site', 'name', 'slug']
help_texts = {
'name': 'Name of VLAN group',
}
class VLANGroupFilterForm(BootstrapMixin, forms.Form): class VLANGroupFilterForm(BootstrapMixin, forms.Form):
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
@@ -934,14 +898,5 @@ class ServiceForm(BootstrapMixin, forms.ModelForm):
super(ServiceForm, self).__init__(*args, **kwargs) super(ServiceForm, self).__init__(*args, **kwargs)
# Limit IP address choices to those assigned to interfaces of the parent device/VM # Limit IP address choices to those assigned to interfaces of the parent device
if self.instance.device: self.fields['ipaddresses'].queryset = IPAddress.objects.filter(interface__device=self.instance.device)
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
interface__device=self.instance.device
)
elif self.instance.virtual_machine:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
interface__virtual_machine=self.instance.virtual_machine
)
else:
self.fields['ipaddresses'].choices = []

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db.models import Lookup, Transform, IntegerField from django.db.models import Lookup, Transform, IntegerField
from django.db.models import lookups from django.db.models.lookups import BuiltinLookup
class NetFieldDecoratorMixin(object): class NetFieldDecoratorMixin(object):
@@ -13,27 +13,27 @@ class NetFieldDecoratorMixin(object):
return lhs_string, lhs_params return lhs_string, lhs_params
class EndsWith(NetFieldDecoratorMixin, lookups.EndsWith): class EndsWith(NetFieldDecoratorMixin, BuiltinLookup):
lookup_name = 'endswith' lookup_name = 'endswith'
class IEndsWith(NetFieldDecoratorMixin, lookups.IEndsWith): class IEndsWith(NetFieldDecoratorMixin, BuiltinLookup):
lookup_name = 'iendswith' lookup_name = 'iendswith'
class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith): class StartsWith(NetFieldDecoratorMixin, BuiltinLookup):
lookup_name = 'startswith' lookup_name = 'startswith'
class IStartsWith(NetFieldDecoratorMixin, lookups.IStartsWith): class IStartsWith(NetFieldDecoratorMixin, BuiltinLookup):
lookup_name = 'istartswith' lookup_name = 'istartswith'
class Regex(NetFieldDecoratorMixin, lookups.Regex): class Regex(NetFieldDecoratorMixin, BuiltinLookup):
lookup_name = 'regex' lookup_name = 'regex'
class IRegex(NetFieldDecoratorMixin, lookups.IRegex): class IRegex(NetFieldDecoratorMixin, BuiltinLookup):
lookup_name = 'iregex' lookup_name = 'iregex'

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-08-03 19:37
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ipam', '0017_ipaddress_roles'),
]
operations = [
migrations.AlterUniqueTogether(
name='service',
unique_together=set([]),
),
]

View File

@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-31 15:44
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0001_virtualization'),
('ipam', '0018_remove_service_uniqueness_constraint'),
]
operations = [
migrations.AlterModelOptions(
name='service',
options={'ordering': ['protocol', 'port']},
),
migrations.AddField(
model_name='service',
name='virtual_machine',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='virtualization.VirtualMachine'),
),
migrations.AlterField(
model_name='service',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'),
),
]

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-09 20:02
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0019_virtualization'),
]
operations = [
migrations.AlterField(
model_name='ipaddress',
name='role',
field=models.PositiveSmallIntegerField(blank=True, choices=[(10, 'Loopback'), (20, 'Secondary'), (30, 'Anycast'), (40, 'VIP'), (41, 'VRRP'), (42, 'HSRP'), (43, 'GLBP'), (44, 'CARP')], help_text='The functional role of this IP', null=True, verbose_name='Role'),
),
]

View File

@@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import netaddr import netaddr
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -14,10 +14,10 @@ from dcim.models import Interface
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.sql import NullsFirstQuerySet
from utilities.utils import csv_format from utilities.utils import csv_format
from .constants import * from .constants import *
from .fields import IPNetworkField, IPAddressField from .fields import IPNetworkField, IPAddressField
from .querysets import PrefixQuerySet
@python_2_unicode_compatible @python_2_unicode_compatible
@@ -188,6 +188,41 @@ class Role(models.Model):
return self.vlans.count() return self.vlans.count()
class PrefixQuerySet(NullsFirstQuerySet):
def annotate_depth(self, limit=None):
"""
Iterate through a QuerySet of Prefixes and annotate the hierarchical level of each. While it would be preferable
to do this using .extra() on the QuerySet to count the unique parents of each prefix, that approach introduces
performance issues at scale.
Because we're adding a non-field attribute to the model, annotation must be made *after* any QuerySet
modifications.
"""
queryset = self
stack = []
for p in queryset:
try:
prev_p = stack[-1]
except IndexError:
prev_p = None
if prev_p is not None:
while (p.prefix not in prev_p.prefix) or p.prefix == prev_p.prefix:
stack.pop()
try:
prev_p = stack[-1]
except IndexError:
prev_p = None
break
if prev_p is not None:
prev_p.has_children = True
stack.append(p)
p.depth = len(stack) - 1
if limit is None:
return queryset
return list(filter(lambda p: p.depth <= limit, queryset))
@python_2_unicode_compatible @python_2_unicode_compatible
class Prefix(CreatedUpdatedModel, CustomFieldModel): class Prefix(CreatedUpdatedModel, CustomFieldModel):
""" """
@@ -285,7 +320,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
""" """
Return all IPAddresses within this Prefix. Return all IPAddresses within this Prefix.
""" """
return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf) return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf)
def get_available_ips(self): def get_available_ips(self):
""" """
@@ -304,16 +339,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
return available_ips return available_ips
def get_first_available_ip(self):
"""
Return the first available IP within the prefix (or None).
"""
available_ips = self.get_available_ips()
if available_ips:
return '{}/{}'.format(next(available_ips.__iter__()), self.prefix.prefixlen)
else:
return None
def get_utilization(self): def get_utilization(self):
""" """
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
@@ -324,7 +349,9 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
child_prefixes = netaddr.IPSet([p.prefix for p in queryset]) child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100) return int(float(child_prefixes.size) / self.prefix.size * 100)
else: else:
child_count = self.get_child_ips().count() child_count = IPAddress.objects.filter(
address__net_contained_or_equal=str(self.prefix), vrf=self.vrf
).count()
prefix_size = self.prefix.size prefix_size = self.prefix.size
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool: if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2 prefix_size -= 2
@@ -391,8 +418,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
objects = IPAddressManager() objects = IPAddressManager()
csv_headers = [ csv_headers = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary', 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description',
'description',
] ]
class Meta: class Meta:
@@ -447,7 +473,6 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
self.get_status_display(), self.get_status_display(),
self.get_role_display(), self.get_role_display(),
self.device.identifier if self.device else None, self.device.identifier if self.device else None,
self.virtual_machine.name if self.virtual_machine else None,
self.interface.name if self.interface else None, self.interface.name if self.interface else None,
is_primary, is_primary,
self.description, self.description,
@@ -459,18 +484,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
return self.interface.device return self.interface.device
return None return None
@property
def virtual_machine(self):
if self.interface:
return self.interface.virtual_machine
return None
def get_status_class(self): def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status] return STATUS_CHOICE_CLASSES[self.status]
def get_role_class(self):
return ROLE_CHOICE_CLASSES[self.role]
@python_2_unicode_compatible @python_2_unicode_compatible
class VLANGroup(models.Model): class VLANGroup(models.Model):
@@ -496,16 +512,6 @@ class VLANGroup(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
def get_next_available_vid(self):
"""
Return the first available VLAN ID (1-4094) in the group.
"""
vids = [vlan['vid'] for vlan in self.vlans.order_by('vid').values('vid')]
for i in range(1, 4095):
if i not in vids:
return i
return None
@python_2_unicode_compatible @python_2_unicode_compatible
class VLAN(CreatedUpdatedModel, CustomFieldModel): class VLAN(CreatedUpdatedModel, CustomFieldModel):
@@ -580,59 +586,21 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
@python_2_unicode_compatible @python_2_unicode_compatible
class Service(CreatedUpdatedModel): class Service(CreatedUpdatedModel):
""" """
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied
optionally be tied to one or more specific IPAddresses belonging to its parent. to one or more specific IPAddresses belonging to the Device.
""" """
device = models.ForeignKey( device = models.ForeignKey('dcim.Device', related_name='services', on_delete=models.CASCADE, verbose_name='device')
to='dcim.Device', name = models.CharField(max_length=30)
on_delete=models.CASCADE, protocol = models.PositiveSmallIntegerField(choices=IP_PROTOCOL_CHOICES)
related_name='services', port = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(65535)],
verbose_name='device', verbose_name='Port number')
null=True, ipaddresses = models.ManyToManyField('ipam.IPAddress', related_name='services', blank=True,
blank=True verbose_name='IP addresses')
) description = models.CharField(max_length=100, blank=True)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='services',
null=True,
blank=True
)
name = models.CharField(
max_length=30
)
protocol = models.PositiveSmallIntegerField(
choices=IP_PROTOCOL_CHOICES
)
port = models.PositiveIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(65535)],
verbose_name='Port number'
)
ipaddresses = models.ManyToManyField(
to='ipam.IPAddress',
related_name='services',
blank=True,
verbose_name='IP addresses'
)
description = models.CharField(
max_length=100,
blank=True
)
class Meta: class Meta:
ordering = ['protocol', 'port'] ordering = ['device', 'protocol', 'port']
unique_together = ['device', 'protocol', 'port']
def __str__(self): def __str__(self):
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
@property
def parent(self):
return self.device or self.virtual_machine
def clean(self):
# A Service must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine:
raise ValidationError("A service cannot be associated with both a device and a virtual machine.")
if not self.device and not self.virtual_machine:
raise ValidationError("A service must be associated with either a device or a virtual machine.")

View File

@@ -1,38 +0,0 @@
from __future__ import unicode_literals
from utilities.sql import NullsFirstQuerySet
class PrefixQuerySet(NullsFirstQuerySet):
def annotate_depth(self, limit=None):
"""
Iterate through a QuerySet of Prefixes and annotate the hierarchical level of each. While it would be preferable
to do this using .extra() on the QuerySet to count the unique parents of each prefix, that approach introduces
performance issues at scale.
Because we're adding a non-field attribute to the model, annotation must be made *after* any QuerySet
modifications.
"""
queryset = self
stack = []
for p in queryset:
try:
prev_p = stack[-1]
except IndexError:
prev_p = None
if prev_p is not None:
while (p.prefix not in prev_p.prefix) or p.prefix == prev_p.prefix:
stack.pop()
try:
prev_p = stack[-1]
except IndexError:
prev_p = None
break
if prev_p is not None:
prev_p.has_children = True
stack.append(p)
p.depth = len(stack) - 1
if limit is None:
return queryset
return list(filter(lambda p: p.depth <= limit, queryset))

View File

@@ -3,9 +3,10 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
RIR_UTILIZATION = """ RIR_UTILIZATION = """
<div class="progress"> <div class="progress">
{% if record.stats.total %} {% if record.stats.total %}
@@ -33,7 +34,7 @@ RIR_ACTIONS = """
UTILIZATION_GRAPH = """ UTILIZATION_GRAPH = """
{% load helpers %} {% load helpers %}
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}&mdash;{% endif %} {% if record.pk %}{% utilization_graph value %}{% else %}&mdash;{% endif %}
""" """
ROLE_ACTIONS = """ ROLE_ACTIONS = """
@@ -44,9 +45,9 @@ ROLE_ACTIONS = """
PREFIX_LINK = """ PREFIX_LINK = """
{% if record.has_children %} {% if record.has_children %}
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a> <span style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
{% else %} {% else %}
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px"> <span style="padding-left: {{ record.depth }}9px">
{% endif %} {% endif %}
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a> <a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
</span> </span>
@@ -70,19 +71,16 @@ IPADDRESS_LINK = """
{% if record.pk %} {% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a> <a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
{% elif perms.ipam.add_ipaddress %} {% elif perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.tenant %}&tenant={{ prefix.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a> <a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
{% else %} {% else %}
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
{% endif %} {% endif %}
""" """
IPADDRESS_ASSIGN_LINK = """ IPADDRESS_DEVICE = """
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
"""
IPADDRESS_PARENT = """
{% if record.interface %} {% if record.interface %}
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a> <a href="{{ record.interface.device.get_absolute_url }}">{{ record.interface.device }}</a>
({{ record.interface.name }})
{% else %} {% else %}
&mdash; &mdash;
{% endif %} {% endif %}
@@ -123,13 +121,6 @@ VLAN_ROLE_LINK = """
""" """
VLANGROUP_ACTIONS = """ VLANGROUP_ACTIONS = """
{% with next_vid=record.get_next_available_vid %}
{% if next_vid and perms.ipam.add_vlan %}
<a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
</a>
{% endif %}
{% endwith %}
{% if perms.ipam.change_vlangroup %} {% if perms.ipam.change_vlangroup %}
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
@@ -161,6 +152,16 @@ class VRFTable(BaseTable):
fields = ('pk', 'name', 'rd', 'tenant', 'description') fields = ('pk', 'name', 'rd', 'tenant', 'description')
class VRFSearchTable(SearchTable):
name = tables.LinkColumn()
rd = tables.Column(verbose_name='RD')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(SearchTable.Meta):
model = VRF
fields = ('name', 'rd', 'tenant', 'description')
# #
# RIRs # RIRs
# #
@@ -170,14 +171,6 @@ class RIRTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
is_private = tables.BooleanColumn(verbose_name='Private') is_private = tables.BooleanColumn(verbose_name='Private')
aggregate_count = tables.Column(verbose_name='Aggregates') aggregate_count = tables.Column(verbose_name='Aggregates')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = RIR
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'actions')
class RIRDetailTable(RIRTable):
stats_total = tables.Column(accessor='stats.total', verbose_name='Total', stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
footer=lambda table: sum(r.stats['total'] for r in table.data)) footer=lambda table: sum(r.stats['total'] for r in table.data))
stats_active = tables.Column(accessor='stats.active', verbose_name='Active', stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
@@ -189,12 +182,12 @@ class RIRDetailTable(RIRTable):
stats_available = tables.Column(accessor='stats.available', verbose_name='Available', stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
footer=lambda table: sum(r.stats['available'] for r in table.data)) footer=lambda table: sum(r.stats['available'] for r in table.data))
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization') utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(RIRTable.Meta): class Meta(BaseTable.Meta):
fields = ( model = RIR
'pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', fields = ('pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
'stats_deprecated', 'stats_available', 'utilization', 'actions', 'stats_deprecated', 'stats_available', 'utilization', 'actions')
)
# #
@@ -204,19 +197,22 @@ class RIRDetailTable(RIRTable):
class AggregateTable(BaseTable): class AggregateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
prefix = tables.LinkColumn(verbose_name='Aggregate') prefix = tables.LinkColumn(verbose_name='Aggregate')
child_count = tables.Column(verbose_name='Prefixes')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added') date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Aggregate model = Aggregate
fields = ('pk', 'prefix', 'rir', 'date_added', 'description') fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
class AggregateDetailTable(AggregateTable): class AggregateSearchTable(SearchTable):
child_count = tables.Column(verbose_name='Prefixes') prefix = tables.LinkColumn(verbose_name='Aggregate')
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
class Meta(AggregateTable.Meta): class Meta(SearchTable.Meta):
fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description') model = Aggregate
fields = ('prefix', 'rir', 'date_added', 'description')
# #
@@ -245,6 +241,7 @@ class PrefixTable(BaseTable):
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
status = tables.TemplateColumn(STATUS_LABEL) status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
tenant = tables.TemplateColumn(TENANT_LINK) tenant = tables.TemplateColumn(TENANT_LINK)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
@@ -252,17 +249,37 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Prefix model = Prefix
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
row_attrs = { row_attrs = {
'class': lambda record: 'success' if not record.pk else '', 'class': lambda record: 'success' if not record.pk else '',
} }
class PrefixDetailTable(PrefixTable): class PrefixBriefTable(BaseTable):
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False) prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF)
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
status = tables.TemplateColumn(STATUS_LABEL)
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')])
class Meta(PrefixTable.Meta): class Meta(BaseTable.Meta):
fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description') model = Prefix
fields = ('prefix', 'vrf', 'status', 'site', 'vlan', 'role')
orderable = False
class PrefixSearchTable(SearchTable):
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
role = tables.TemplateColumn(PREFIX_ROLE_LINK)
class Meta(SearchTable.Meta):
model = Prefix
fields = ('prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
# #
@@ -272,41 +289,46 @@ class PrefixDetailTable(PrefixTable):
class IPAddressTable(BaseTable): class IPAddressTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
status = tables.TemplateColumn(STATUS_LABEL) status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK) tenant = tables.TemplateColumn(TENANT_LINK)
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False) nat_inside = tables.LinkColumn(
interface = tables.Column(orderable=False) 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
)
device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description') fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'description')
row_attrs = { row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '', 'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
} }
class IPAddressDetailTable(IPAddressTable): class IPAddressBriefTable(BaseTable):
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
interface = tables.Column(orderable=False)
nat_inside = tables.LinkColumn( nat_inside = tables.LinkColumn(
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
) )
class Meta(IPAddressTable.Meta):
fields = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'description',
)
class IPAddressAssignTable(BaseTable):
address = tables.TemplateColumn(IPADDRESS_ASSIGN_LINK, verbose_name='IP Address')
status = tables.TemplateColumn(STATUS_LABEL)
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
interface = tables.Column(orderable=False)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface') fields = ('address', 'device', 'interface', 'nat_inside')
orderable = False
class IPAddressSearchTable(SearchTable):
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK)
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
interface = tables.Column(orderable=False)
class Meta(SearchTable.Meta):
model = IPAddress
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
# #
@@ -336,17 +358,24 @@ class VLANTable(BaseTable):
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
status = tables.TemplateColumn(STATUS_LABEL) status = tables.TemplateColumn(STATUS_LABEL)
role = tables.TemplateColumn(VLAN_ROLE_LINK) role = tables.TemplateColumn(VLAN_ROLE_LINK)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VLAN model = VLAN
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
class VLANDetailTable(VLANTable):
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
class Meta(VLANTable.Meta):
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
class VLANSearchTable(SearchTable):
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
status = tables.TemplateColumn(STATUS_LABEL)
role = tables.TemplateColumn(VLAN_ROLE_LINK)
class Meta(SearchTable.Meta):
model = VLAN
fields = ('vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')

View File

@@ -1,14 +1,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.urls import reverse
from netaddr import IPNetwork from netaddr import IPNetwork
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from django.contrib.auth.models import User
from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.constants import IP_PROTOCOL_TCP, IP_PROTOCOL_UDP from ipam.models import (
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF Aggregate, IPAddress, IP_PROTOCOL_TCP, IP_PROTOCOL_UDP, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF,
)
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import netaddr import netaddr
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings from django.test import TestCase, override_settings

View File

@@ -21,7 +21,6 @@ urlpatterns = [
# RIRs # RIRs
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'), url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'), url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'),
url(r'^rirs/import/$', views.RIRBulkImportView.as_view(), name='rir_import'),
url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'), url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
@@ -38,7 +37,6 @@ urlpatterns = [
# Roles # Roles
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'), url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'), url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'),
url(r'^roles/import/$', views.RoleBulkImportView.as_view(), name='role_import'),
url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'), url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
@@ -60,7 +58,6 @@ urlpatterns = [
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'), url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
@@ -68,7 +65,6 @@ urlpatterns = [
# VLAN groups # VLAN groups
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'), url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'), url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/import/$', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),

View File

@@ -1,23 +1,25 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django_tables2 import RequestConfig
import netaddr import netaddr
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count, Q from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.views.generic import View from django.views.generic import View
from django_tables2 import RequestConfig
from dcim.models import Device, Interface from dcim.models import Device
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from virtualization.models import VirtualMachine
from . import filters, forms, tables from . import filters, forms, tables
from .constants import IPADDRESS_ROLE_ANYCAST, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED from .models import (
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
Service, VLAN, VLANGroup, VRF,
)
def add_available_prefixes(parent, prefix_list): def add_available_prefixes(parent, prefix_list):
@@ -101,8 +103,8 @@ class VRFView(View):
def get(self, request, pk): def get(self, request, pk):
vrf = get_object_or_404(VRF.objects.all(), pk=pk) vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefix_table = tables.PrefixTable( prefix_table = tables.PrefixBriefTable(
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
) )
prefix_table.exclude = ('vrf',) prefix_table.exclude = ('vrf',)
@@ -115,7 +117,7 @@ class VRFView(View):
class VRFCreateView(PermissionRequiredMixin, ObjectEditView): class VRFCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vrf' permission_required = 'ipam.add_vrf'
model = VRF model = VRF
model_form = forms.VRFForm form_class = forms.VRFForm
template_name = 'ipam/vrf_edit.html' template_name = 'ipam/vrf_edit.html'
default_return_url = 'ipam:vrf_list' default_return_url = 'ipam:vrf_list'
@@ -140,19 +142,16 @@ class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView): class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vrf' permission_required = 'ipam.change_vrf'
cls = VRF cls = VRF
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter filter = filters.VRFFilter
table = tables.VRFTable
form = forms.VRFBulkEditForm form = forms.VRFBulkEditForm
template_name = 'ipam/vrf_bulk_edit.html'
default_return_url = 'ipam:vrf_list' default_return_url = 'ipam:vrf_list'
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vrf' permission_required = 'ipam.delete_vrf'
cls = VRF cls = VRF
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter filter = filters.VRFFilter
table = tables.VRFTable
default_return_url = 'ipam:vrf_list' default_return_url = 'ipam:vrf_list'
@@ -164,7 +163,7 @@ class RIRListView(ObjectListView):
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')) queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter filter = filters.RIRFilter
filter_form = forms.RIRFilterForm filter_form = forms.RIRFilterForm
table = tables.RIRDetailTable table = tables.RIRTable
template_name = 'ipam/rir_list.html' template_name = 'ipam/rir_list.html'
def alter_queryset(self, request): def alter_queryset(self, request):
@@ -247,7 +246,7 @@ class RIRListView(ObjectListView):
class RIRCreateView(PermissionRequiredMixin, ObjectEditView): class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_rir' permission_required = 'ipam.add_rir'
model = RIR model = RIR
model_form = forms.RIRForm form_class = forms.RIRForm
def get_return_url(self, request, obj): def get_return_url(self, request, obj):
return reverse('ipam:rir_list') return reverse('ipam:rir_list')
@@ -257,19 +256,10 @@ class RIREditView(RIRCreateView):
permission_required = 'ipam.change_rir' permission_required = 'ipam.change_rir'
class RIRBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_rir'
model_form = forms.RIRCSVForm
table = tables.RIRTable
default_return_url = 'ipam:rir_list'
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_rir' permission_required = 'ipam.delete_rir'
cls = RIR cls = RIR
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter filter = filters.RIRFilter
table = tables.RIRTable
default_return_url = 'ipam:rir_list' default_return_url = 'ipam:rir_list'
@@ -283,19 +273,18 @@ class AggregateListView(ObjectListView):
}) })
filter = filters.AggregateFilter filter = filters.AggregateFilter
filter_form = forms.AggregateFilterForm filter_form = forms.AggregateFilterForm
table = tables.AggregateDetailTable table = tables.AggregateTable
template_name = 'ipam/aggregate_list.html' template_name = 'ipam/aggregate_list.html'
def extra_context(self): def extra_context(self):
ipv4_total = 0 ipv4_total = 0
ipv6_total = 0 ipv6_total = 0
for aggregate in self.queryset: for a in self.queryset:
if aggregate.prefix.version == 6: if a.prefix.version == 4:
# Report equivalent /64s for IPv6 to keep things sane ipv4_total += a.prefix.size
ipv6_total += int(aggregate.prefix.size / 2 ** 64) elif a.prefix.version == 6:
else: ipv6_total += a.prefix.size / 2 ** 64
ipv4_total += aggregate.prefix.size
return { return {
'ipv4_total': ipv4_total, 'ipv4_total': ipv4_total,
@@ -319,9 +308,9 @@ class AggregateView(View):
) )
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes) child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
prefix_table = tables.PrefixDetailTable(child_prefixes) prefix_table = tables.PrefixTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
prefix_table.columns.show('pk') prefix_table.base_columns['pk'].visible = True
paginate = { paginate = {
'klass': EnhancedPaginator, 'klass': EnhancedPaginator,
@@ -346,7 +335,7 @@ class AggregateView(View):
class AggregateCreateView(PermissionRequiredMixin, ObjectEditView): class AggregateCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_aggregate' permission_required = 'ipam.add_aggregate'
model = Aggregate model = Aggregate
model_form = forms.AggregateForm form_class = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html' template_name = 'ipam/aggregate_edit.html'
default_return_url = 'ipam:aggregate_list' default_return_url = 'ipam:aggregate_list'
@@ -371,19 +360,16 @@ class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView): class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_aggregate' permission_required = 'ipam.change_aggregate'
cls = Aggregate cls = Aggregate
queryset = Aggregate.objects.select_related('rir')
filter = filters.AggregateFilter filter = filters.AggregateFilter
table = tables.AggregateTable
form = forms.AggregateBulkEditForm form = forms.AggregateBulkEditForm
template_name = 'ipam/aggregate_bulk_edit.html'
default_return_url = 'ipam:aggregate_list' default_return_url = 'ipam:aggregate_list'
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_aggregate' permission_required = 'ipam.delete_aggregate'
cls = Aggregate cls = Aggregate
queryset = Aggregate.objects.select_related('rir')
filter = filters.AggregateFilter filter = filters.AggregateFilter
table = tables.AggregateTable
default_return_url = 'ipam:aggregate_list' default_return_url = 'ipam:aggregate_list'
@@ -400,7 +386,7 @@ class RoleListView(ObjectListView):
class RoleCreateView(PermissionRequiredMixin, ObjectEditView): class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_role' permission_required = 'ipam.add_role'
model = Role model = Role
model_form = forms.RoleForm form_class = forms.RoleForm
def get_return_url(self, request, obj): def get_return_url(self, request, obj):
return reverse('ipam:role_list') return reverse('ipam:role_list')
@@ -410,17 +396,9 @@ class RoleEditView(RoleCreateView):
permission_required = 'ipam.change_role' permission_required = 'ipam.change_role'
class RoleBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_role'
model_form = forms.RoleCSVForm
table = tables.RoleTable
default_return_url = 'ipam:role_list'
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role' permission_required = 'ipam.delete_role'
cls = Role cls = Role
table = tables.RoleTable
default_return_url = 'ipam:role_list' default_return_url = 'ipam:role_list'
@@ -432,7 +410,7 @@ class PrefixListView(ObjectListView):
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm filter_form = forms.PrefixFilterForm
table = tables.PrefixDetailTable table = tables.PrefixTable
template_name = 'ipam/prefix_list.html' template_name = 'ipam/prefix_list.html'
def alter_queryset(self, request): def alter_queryset(self, request):
@@ -455,7 +433,9 @@ class PrefixView(View):
aggregate = None aggregate = None
# Count child IP addresses # Count child IP addresses
ipaddress_count = prefix.get_child_ips().count() ipaddress_count = IPAddress.objects.filter(
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
).count()
# Parent prefixes table # Parent prefixes table
parent_prefixes = Prefix.objects.filter( parent_prefixes = Prefix.objects.filter(
@@ -465,7 +445,7 @@ class PrefixView(View):
).select_related( ).select_related(
'site', 'role' 'site', 'role'
).annotate_depth() ).annotate_depth()
parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False) parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
parent_prefix_table.exclude = ('vrf',) parent_prefix_table.exclude = ('vrf',)
# Duplicate prefixes table # Duplicate prefixes table
@@ -476,20 +456,20 @@ class PrefixView(View):
).select_related( ).select_related(
'site', 'role' 'site', 'role'
) )
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False) duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
duplicate_prefix_table.exclude = ('vrf',) duplicate_prefix_table.exclude = ('vrf',)
# Child prefixes table # Child prefixes table
child_prefixes = Prefix.objects.filter( child_prefixes = Prefix.objects.filter(
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix) vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
).select_related( ).select_related(
'site', 'vlan', 'role', 'site', 'role'
).annotate_depth(limit=0) ).annotate_depth(limit=0)
if child_prefixes: if child_prefixes:
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
child_prefix_table = tables.PrefixDetailTable(child_prefixes) child_prefix_table = tables.PrefixTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
child_prefix_table.columns.show('pk') child_prefix_table.base_columns['pk'].visible = True
paginate = { paginate = {
'klass': EnhancedPaginator, 'klass': EnhancedPaginator,
@@ -511,7 +491,6 @@ class PrefixView(View):
'parent_prefix_table': parent_prefix_table, 'parent_prefix_table': parent_prefix_table,
'child_prefix_table': child_prefix_table, 'child_prefix_table': child_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table, 'duplicate_prefix_table': duplicate_prefix_table,
'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf or '0', prefix.prefix),
'permissions': permissions, 'permissions': permissions,
'return_url': prefix.get_absolute_url(), 'return_url': prefix.get_absolute_url(),
}) })
@@ -524,14 +503,16 @@ class PrefixIPAddressesView(View):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk) prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
# Find all IPAddresses belonging to this Prefix # Find all IPAddresses belonging to this Prefix
ipaddresses = prefix.get_child_ips().select_related( ipaddresses = IPAddress.objects.filter(
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
).select_related(
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for' 'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
) )
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool) ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
ip_table = tables.IPAddressTable(ipaddresses) ip_table = tables.IPAddressTable(ipaddresses)
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'): if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
ip_table.columns.show('pk') ip_table.base_columns['pk'].visible = True
paginate = { paginate = {
'klass': EnhancedPaginator, 'klass': EnhancedPaginator,
@@ -550,14 +531,14 @@ class PrefixIPAddressesView(View):
'prefix': prefix, 'prefix': prefix,
'ip_table': ip_table, 'ip_table': ip_table,
'permissions': permissions, 'permissions': permissions,
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix), 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
}) })
class PrefixCreateView(PermissionRequiredMixin, ObjectEditView): class PrefixCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_prefix' permission_required = 'ipam.add_prefix'
model = Prefix model = Prefix
model_form = forms.PrefixForm form_class = forms.PrefixForm
template_name = 'ipam/prefix_edit.html' template_name = 'ipam/prefix_edit.html'
default_return_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
@@ -583,19 +564,16 @@ class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView): class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_prefix' permission_required = 'ipam.change_prefix'
cls = Prefix cls = Prefix
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter filter = filters.PrefixFilter
table = tables.PrefixTable
form = forms.PrefixBulkEditForm form = forms.PrefixBulkEditForm
template_name = 'ipam/prefix_bulk_edit.html'
default_return_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_prefix' permission_required = 'ipam.delete_prefix'
cls = Prefix cls = Prefix
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter filter = filters.PrefixFilter
table = tables.PrefixTable
default_return_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
@@ -604,14 +582,10 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class IPAddressListView(ObjectListView): class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.select_related( queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
'vrf__tenant', 'tenant', 'nat_inside'
).prefetch_related(
'interface__device', 'interface__virtual_machine'
)
filter = filters.IPAddressFilter filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm filter_form = forms.IPAddressFilterForm
table = tables.IPAddressDetailTable table = tables.IPAddressTable
template_name = 'ipam/ipaddress_list.html' template_name = 'ipam/ipaddress_list.html'
@@ -619,7 +593,7 @@ class IPAddressView(View):
def get(self, request, pk): def get(self, request, pk):
ipaddress = get_object_or_404(IPAddress.objects.select_related('vrf__tenant', 'tenant'), pk=pk) ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
# Parent prefixes table # Parent prefixes table
parent_prefixes = Prefix.objects.filter( parent_prefixes = Prefix.objects.filter(
@@ -627,7 +601,7 @@ class IPAddressView(View):
).select_related( ).select_related(
'site', 'role' 'site', 'role'
) )
parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False) parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
parent_prefixes_table.exclude = ('vrf',) parent_prefixes_table.exclude = ('vrf',)
# Duplicate IPs table # Duplicate IPs table
@@ -636,24 +610,19 @@ class IPAddressView(View):
).exclude( ).exclude(
pk=ipaddress.pk pk=ipaddress.pk
).select_related( ).select_related(
'nat_inside' 'interface__device', 'nat_inside'
).prefetch_related(
'interface__device'
) )
# Exclude anycast IPs if this IP is anycast duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
if ipaddress.role == IPADDRESS_ROLE_ANYCAST:
duplicate_ips = duplicate_ips.exclude(role=IPADDRESS_ROLE_ANYCAST)
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
# Related IP table # Related IP table
related_ips = IPAddress.objects.prefetch_related( related_ips = IPAddress.objects.select_related(
'interface__device' 'interface__device'
).exclude( ).exclude(
address=str(ipaddress.address) address=str(ipaddress.address)
).filter( ).filter(
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address) vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
) )
related_ips_table = tables.IPAddressTable(list(related_ips), orderable=False) related_ips_table = tables.IPAddressBriefTable(list(related_ips))
return render(request, 'ipam/ipaddress.html', { return render(request, 'ipam/ipaddress.html', {
'ipaddress': ipaddress, 'ipaddress': ipaddress,
@@ -666,71 +635,15 @@ class IPAddressView(View):
class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView): class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_ipaddress' permission_required = 'ipam.add_ipaddress'
model = IPAddress model = IPAddress
model_form = forms.IPAddressForm form_class = forms.IPAddressForm
template_name = 'ipam/ipaddress_edit.html' template_name = 'ipam/ipaddress_edit.html'
default_return_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
def alter_obj(self, obj, request, url_args, url_kwargs):
interface_id = request.GET.get('interface')
if interface_id:
try:
obj.interface = Interface.objects.get(pk=interface_id)
except (ValueError, Interface.DoesNotExist):
pass
return obj
class IPAddressEditView(IPAddressCreateView): class IPAddressEditView(IPAddressCreateView):
permission_required = 'ipam.change_ipaddress' permission_required = 'ipam.change_ipaddress'
class IPAddressAssignView(PermissionRequiredMixin, View):
"""
Search for IPAddresses to be assigned to an Interface.
"""
permission_required = 'ipam.change_ipaddress'
def dispatch(self, request, *args, **kwargs):
# Redirect user if an interface has not been provided
if 'interface' not in request.GET:
return redirect('ipam:ipaddress_add')
return super(IPAddressAssignView, self).dispatch(request, *args, **kwargs)
def get(self, request):
form = forms.IPAddressAssignForm()
return render(request, 'ipam/ipaddress_assign.html', {
'form': form,
'return_url': request.GET.get('return_url', ''),
})
def post(self, request):
form = forms.IPAddressAssignForm(request.POST)
table = None
if form.is_valid():
queryset = IPAddress.objects.select_related(
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
).filter(
vrf=form.cleaned_data['vrf'],
address__net_host=form.cleaned_data['address'],
)
table = tables.IPAddressAssignTable(queryset)
return render(request, 'ipam/ipaddress_assign.html', {
'form': form,
'table': table,
'return_url': request.GET.get('return_url', ''),
})
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_ipaddress' permission_required = 'ipam.delete_ipaddress'
model = IPAddress model = IPAddress
@@ -739,7 +652,7 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView): class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
permission_required = 'ipam.add_ipaddress' permission_required = 'ipam.add_ipaddress'
form = forms.IPAddressBulkCreateForm pattern_form = forms.IPAddressPatternForm
model_form = forms.IPAddressBulkAddForm model_form = forms.IPAddressBulkAddForm
pattern_target = 'address' pattern_target = 'address'
template_name = 'ipam/ipaddress_bulk_add.html' template_name = 'ipam/ipaddress_bulk_add.html'
@@ -756,19 +669,16 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView): class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_ipaddress' permission_required = 'ipam.change_ipaddress'
cls = IPAddress cls = IPAddress
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
filter = filters.IPAddressFilter filter = filters.IPAddressFilter
table = tables.IPAddressTable
form = forms.IPAddressBulkEditForm form = forms.IPAddressBulkEditForm
template_name = 'ipam/ipaddress_bulk_edit.html'
default_return_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_ipaddress' permission_required = 'ipam.delete_ipaddress'
cls = IPAddress cls = IPAddress
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
filter = filters.IPAddressFilter filter = filters.IPAddressFilter
table = tables.IPAddressTable
default_return_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
@@ -787,7 +697,7 @@ class VLANGroupListView(ObjectListView):
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView): class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlangroup' permission_required = 'ipam.add_vlangroup'
model = VLANGroup model = VLANGroup
model_form = forms.VLANGroupForm form_class = forms.VLANGroupForm
def get_return_url(self, request, obj): def get_return_url(self, request, obj):
return reverse('ipam:vlangroup_list') return reverse('ipam:vlangroup_list')
@@ -797,19 +707,10 @@ class VLANGroupEditView(VLANGroupCreateView):
permission_required = 'ipam.change_vlangroup' permission_required = 'ipam.change_vlangroup'
class VLANGroupBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_vlangroup'
model_form = forms.VLANGroupCSVForm
table = tables.VLANGroupTable
default_return_url = 'ipam:vlangroup_list'
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup' permission_required = 'ipam.delete_vlangroup'
cls = VLANGroup cls = VLANGroup
queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
filter = filters.VLANGroupFilter filter = filters.VLANGroupFilter
table = tables.VLANGroupTable
default_return_url = 'ipam:vlangroup_list' default_return_url = 'ipam:vlangroup_list'
@@ -821,7 +722,7 @@ class VLANListView(ObjectListView):
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes') queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
filter = filters.VLANFilter filter = filters.VLANFilter
filter_form = forms.VLANFilterForm filter_form = forms.VLANFilterForm
table = tables.VLANDetailTable table = tables.VLANTable
template_name = 'ipam/vlan_list.html' template_name = 'ipam/vlan_list.html'
@@ -833,7 +734,7 @@ class VLANView(View):
'site__region', 'tenant__group', 'role' 'site__region', 'tenant__group', 'role'
), pk=pk) ), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixTable(list(prefixes), orderable=False) prefix_table = tables.PrefixBriefTable(list(prefixes))
prefix_table.exclude = ('vlan',) prefix_table.exclude = ('vlan',)
return render(request, 'ipam/vlan.html', { return render(request, 'ipam/vlan.html', {
@@ -845,7 +746,7 @@ class VLANView(View):
class VLANCreateView(PermissionRequiredMixin, ObjectEditView): class VLANCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlan' permission_required = 'ipam.add_vlan'
model = VLAN model = VLAN
model_form = forms.VLANForm form_class = forms.VLANForm
template_name = 'ipam/vlan_edit.html' template_name = 'ipam/vlan_edit.html'
default_return_url = 'ipam:vlan_list' default_return_url = 'ipam:vlan_list'
@@ -870,19 +771,16 @@ class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView): class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vlan' permission_required = 'ipam.change_vlan'
cls = VLAN cls = VLAN
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.VLANFilter filter = filters.VLANFilter
table = tables.VLANTable
form = forms.VLANBulkEditForm form = forms.VLANBulkEditForm
template_name = 'ipam/vlan_bulk_edit.html'
default_return_url = 'ipam:vlan_list' default_return_url = 'ipam:vlan_list'
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlan' permission_required = 'ipam.delete_vlan'
cls = VLAN cls = VLAN
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.VLANFilter filter = filters.VLANFilter
table = tables.VLANTable
default_return_url = 'ipam:vlan_list' default_return_url = 'ipam:vlan_list'
@@ -893,18 +791,16 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class ServiceCreateView(PermissionRequiredMixin, ObjectEditView): class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_service' permission_required = 'ipam.add_service'
model = Service model = Service
model_form = forms.ServiceForm form_class = forms.ServiceForm
template_name = 'ipam/service_edit.html' template_name = 'ipam/service_edit.html'
def alter_obj(self, obj, request, url_args, url_kwargs): def alter_obj(self, obj, request, url_args, url_kwargs):
if 'device' in url_kwargs: if 'device' in url_kwargs:
obj.device = get_object_or_404(Device, pk=url_kwargs['device']) obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
elif 'virtualmachine' in url_kwargs:
obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine'])
return obj return obj
def get_return_url(self, request, obj): def get_return_url(self, request, obj):
return obj.parent.get_absolute_url() return obj.device.get_absolute_url()
class ServiceEditView(ServiceCreateView): class ServiceEditView(ServiceCreateView):

View File

@@ -1,147 +0,0 @@
from __future__ import unicode_literals
from rest_framework import authentication, exceptions
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import DjangoModelPermissions, SAFE_METHODS
from rest_framework.renderers import BrowsableAPIRenderer
from rest_framework.utils import formatting
from users.models import Token
#
# Renderers
#
class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
"""
Override the built-in BrowsableAPIRenderer to disable HTML forms.
"""
def show_form_for_method(self, *args, **kwargs):
return False
#
# Authentication
#
class TokenAuthentication(authentication.TokenAuthentication):
"""
A custom authentication scheme which enforces Token expiration times.
"""
model = Token
def authenticate_credentials(self, key):
model = self.get_model()
try:
token = model.objects.select_related('user').get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed("Invalid token")
# Enforce the Token's expiration time, if one has been set.
if token.is_expired:
raise exceptions.AuthenticationFailed("Token expired")
if not token.user.is_active:
raise exceptions.AuthenticationFailed("User inactive")
return token.user, token
class TokenPermissions(DjangoModelPermissions):
"""
Custom permissions handler which extends the built-in DjangoModelPermissions to validate a Token's write ability
for unsafe requests (POST/PUT/PATCH/DELETE).
"""
def __init__(self):
# LOGIN_REQUIRED determines whether read-only access is provided to anonymous users.
from django.conf import settings
self.authenticated_users_only = settings.LOGIN_REQUIRED
super(TokenPermissions, self).__init__()
def has_permission(self, request, view):
# If token authentication is in use, verify that the token allows write operations (for unsafe methods).
if request.method not in SAFE_METHODS and isinstance(request.auth, Token):
if not request.auth.write_enabled:
return False
return super(TokenPermissions, self).has_permission(request, view)
#
# Pagination
#
class OptionalLimitOffsetPagination(LimitOffsetPagination):
"""
Override the stock paginator to allow setting limit=0 to disable pagination for a request. This returns all objects
matching a query, but retains the same format as a paginated request. The limit can only be disabled if
MAX_PAGE_SIZE has been set to 0 or None.
"""
def paginate_queryset(self, queryset, request, view=None):
try:
self.count = queryset.count()
except (AttributeError, TypeError):
self.count = len(queryset)
self.limit = self.get_limit(request)
self.offset = self.get_offset(request)
self.request = request
if self.limit and self.count > self.limit and self.template is not None:
self.display_page_controls = True
if self.count == 0 or self.offset > self.count:
return list()
if self.limit:
return list(queryset[self.offset:self.offset + self.limit])
else:
return list(queryset[self.offset:])
def get_limit(self, request):
from django.conf import settings
if self.limit_query_param:
try:
limit = int(request.query_params[self.limit_query_param])
if limit < 0:
raise ValueError()
# Enforce maximum page size, if defined
if settings.MAX_PAGE_SIZE:
if limit == 0:
return settings.MAX_PAGE_SIZE
else:
return min(limit, settings.MAX_PAGE_SIZE)
return limit
except (KeyError, ValueError):
pass
return self.default_limit
#
# Miscellaneous
#
def get_view_name(view_cls, suffix=None):
"""
Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name`.
"""
if hasattr(view_cls, 'queryset'):
# Determine the model name from the queryset.
name = view_cls.queryset.model._meta.verbose_name
name = ' '.join([w[0].upper() + w[1:] for w in name.split()]) # Capitalize each word
else:
# Replicate DRF's built-in behavior.
name = view_cls.__name__
name = formatting.remove_trailing_string(name, 'View')
name = formatting.remove_trailing_string(name, 'ViewSet')
name = formatting.camelcase_to_spaces(name)
if suffix:
name += ' ' + suffix
return name

View File

@@ -0,0 +1,79 @@
import os
#########################
# #
# Required settings #
# #
#########################
# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
#
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(' ')
# PostgreSQL database configuration.
DATABASE = {
'NAME': os.environ.get('DB_NAME', 'netbox'), # Database name
'USER': os.environ.get('DB_USER', ''), # PostgreSQL username
'PASSWORD': os.environ.get('DB_PASSWORD', ''), # PostgreSQL password
'HOST': os.environ.get('DB_HOST', 'localhost'), # Database server
'PORT': os.environ.get('DB_PORT', ''), # Database port (leave blank for default)
}
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
# symbols. NetBox will not run without this defined. For more information, see
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY
SECRET_KEY = os.environ.get('SECRET_KEY', '')
#########################
# #
# Optional settings #
# #
#########################
# Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
# application errors (assuming correct email settings are provided).
ADMINS = [
# ['John Doe', 'jdoe@example.com'],
]
# Email settings
EMAIL = {
'SERVER': os.environ.get('EMAIL_SERVER', 'localhost'),
'PORT': os.environ.get('EMAIL_PORT', 25),
'USERNAME': os.environ.get('EMAIL_USERNAME', ''),
'PASSWORD': os.environ.get('EMAIL_PASSWORD', ''),
'TIMEOUT': os.environ.get('EMAIL_TIMEOUT', 10), # seconds
'FROM_EMAIL': os.environ.get('EMAIL_FROM', ''),
}
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
LOGIN_REQUIRED = os.environ.get('LOGIN_REQUIRED', False)
# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
# BASE_PATH = 'netbox/'
BASE_PATH = os.environ.get('BASE_PATH', '')
# Setting this to True will display a "maintenance mode" banner at the top of every page.
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
# Credentials that NetBox will use to access live devices.
NETBOX_USERNAME = os.environ.get('NETBOX_USERNAME', '')
NETBOX_PASSWORD = os.environ.get('NETBOX_PASSWORD', '')
# Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50)
# Time zone (default: UTC)
TIME_ZONE = os.environ.get('TIME_ZONE', 'UTC')
# Date/time formatting. See the following link for supported formats:
# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
DATE_FORMAT = os.environ.get('DATE_FORMAT', 'N j, Y')
SHORT_DATE_FORMAT = os.environ.get('SHORT_DATE_FORMAT', 'Y-m-d')
TIME_FORMAT = os.environ.get('TIME_FORMAT', 'g:i a')
SHORT_TIME_FORMAT = os.environ.get('SHORT_TIME_FORMAT', 'H:i:s')
DATETIME_FORMAT = os.environ.get('DATETIME_FORMAT', 'N j, Y g:i a')
SHORT_DATETIME_FORMAT = os.environ.get('SHORT_DATETIME_FORMAT', 'Y-m-d H:i')

View File

@@ -38,14 +38,11 @@ ADMINS = [
# ['John Doe', 'jdoe@example.com'], # ['John Doe', 'jdoe@example.com'],
] ]
# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same # Optionally display a persistent banner at the top and/or bottom of every page. To display the same content in both
# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. # banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
BANNER_TOP = '' BANNER_TOP = ''
BANNER_BOTTOM = '' BANNER_BOTTOM = ''
# Text to include on the login page above the login form. HTML is allowed.
BANNER_LOGIN = ''
# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set: # Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
# BASE_PATH = 'netbox/' # BASE_PATH = 'netbox/'
BASE_PATH = '' BASE_PATH = ''
@@ -96,20 +93,9 @@ MAINTENANCE_MODE = False
# all objects by specifying "?limit=0". # all objects by specifying "?limit=0".
MAX_PAGE_SIZE = 1000 MAX_PAGE_SIZE = 1000
# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that # Credentials that NetBox will use to access live devices (future use).
# the default value of this setting is derived from the installed location. NETBOX_USERNAME = ''
# MEDIA_ROOT = '/opt/netbox/netbox/media' NETBOX_PASSWORD = ''
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
NAPALM_USERNAME = ''
NAPALM_PASSWORD = ''
# NAPALM timeout (in seconds). (Default: 30)
NAPALM_TIMEOUT = 30
# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
# be provided as a dictionary.
NAPALM_ARGS = {}
# Determine how many objects to display per page within a list. (Default: 50) # Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = 50 PAGINATE_COUNT = 50
@@ -118,10 +104,6 @@ PAGINATE_COUNT = 50
# prefer IPv4 instead. # prefer IPv4 instead.
PREFER_IPV4 = False PREFER_IPV4 = False
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
# this setting is derived from the installed location.
# REPORTS_ROOT = '/opt/netbox/netbox/reports'
# Time zone (default: UTC) # Time zone (default: UTC)
TIME_ZONE = 'UTC' TIME_ZONE = 'UTC'

View File

@@ -4,6 +4,7 @@ from django import forms
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
OBJ_TYPE_CHOICES = ( OBJ_TYPE_CHOICES = (
('', 'All Objects'), ('', 'All Objects'),
('Circuits', ( ('Circuits', (
@@ -29,16 +30,12 @@ OBJ_TYPE_CHOICES = (
('Tenancy', ( ('Tenancy', (
('tenant', 'Tenants'), ('tenant', 'Tenants'),
)), )),
('Virtualization', (
('cluster', 'Clusters'),
('virtualmachine', 'Virtual machines'),
)),
) )
class SearchForm(BootstrapMixin, forms.Form): class SearchForm(BootstrapMixin, forms.Form):
q = forms.CharField( q = forms.CharField(
label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'}) label='Query', widget=forms.TextInput(attrs={'style': 'width: 350px'})
) )
obj_type = forms.ChoiceField( obj_type = forms.ChoiceField(
choices=OBJ_TYPE_CHOICES, required=False, label='Type' choices=OBJ_TYPE_CHOICES, required=False, label='Type'

View File

@@ -13,9 +13,7 @@ except ImportError:
) )
VERSION = '2.2.6' VERSION = '2.1-beta'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Import required configuration parameters # Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
@@ -29,9 +27,8 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
# Import optional configuration parameters # Import optional configuration parameters
ADMINS = getattr(configuration, 'ADMINS', []) ADMINS = getattr(configuration, 'ADMINS', [])
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', '') BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
BANNER_LOGIN = getattr(configuration, 'BANNER_LOGIN', '') BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')
BASE_PATH = getattr(configuration, 'BASE_PATH', '') BASE_PATH = getattr(configuration, 'BASE_PATH', '')
if BASE_PATH: if BASE_PATH:
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
@@ -47,14 +44,10 @@ LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False) LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False) MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000) MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/') NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
@@ -85,15 +78,17 @@ if LDAP_CONFIGURED:
if LDAP_IGNORE_CERT_ERRORS: if LDAP_IGNORE_CERT_ERRORS:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
# Enable logging for django_auth_ldap # Enable logging for django_auth_ldap
ldap_logger = logging.getLogger('django_auth_ldap') logger = logging.getLogger('django_auth_ldap')
ldap_logger.addHandler(logging.StreamHandler()) logger.addHandler(logging.StreamHandler())
ldap_logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
except ImportError: except ImportError:
raise ImproperlyConfigured( raise ImproperlyConfigured(
"LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove " "LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove "
"netbox/ldap_config.py to disable LDAP." "netbox/ldap_config.py to disable LDAP."
) )
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Database # Database
configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'}) configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'})
DATABASES = { DATABASES = {
@@ -133,7 +128,6 @@ INSTALLED_APPS = (
'tenancy', 'tenancy',
'users', 'users',
'utilities', 'utilities',
'virtualization',
) )
# Middleware # Middleware
@@ -148,7 +142,6 @@ MIDDLEWARE = (
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'utilities.middleware.ExceptionHandlingMiddleware',
'utilities.middleware.LoginRequiredMiddleware', 'utilities.middleware.LoginRequiredMiddleware',
'utilities.middleware.APIVersionMiddleware', 'utilities.middleware.APIVersionMiddleware',
) )
@@ -191,6 +184,7 @@ STATICFILES_DIRS = (
) )
# Media # Media
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/{}media/'.format(BASE_PATH) MEDIA_URL = '/{}media/'.format(BASE_PATH)
# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.) # Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
@@ -207,36 +201,35 @@ LOGIN_URL = '/{}login/'.format(BASE_PATH)
# Secrets # Secrets
SECRETS_MIN_PUBKEY_SIZE = 2048 SECRETS_MIN_PUBKEY_SIZE = 2048
# Django filters
FILTERS_NULL_CHOICE_LABEL = 'None'
FILTERS_NULL_CHOICE_VALUE = '0' # Must be a string
# Django REST framework (API) # Django REST framework (API)
REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version
REST_FRAMEWORK = { REST_FRAMEWORK = {
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
'netbox.api.TokenAuthentication', 'utilities.api.TokenAuthentication',
), ),
'DEFAULT_FILTER_BACKENDS': ( 'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.DjangoFilterBackend',
), ),
'DEFAULT_PAGINATION_CLASS': 'netbox.api.OptionalLimitOffsetPagination', 'DEFAULT_PAGINATION_CLASS': 'utilities.api.OptionalLimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': ( 'DEFAULT_PERMISSION_CLASSES': (
'netbox.api.TokenPermissions', 'utilities.api.TokenPermissions',
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'netbox.api.FormlessBrowsableAPIRenderer',
), ),
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION, 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'PAGE_SIZE': PAGINATE_COUNT, 'PAGE_SIZE': PAGINATE_COUNT,
'VIEW_NAME_FUNCTION': 'netbox.api.get_view_name',
} }
# Django debug toolbar # Django debug toolbar
# Disable the templates panel by default due to a performance issue in Django 1.11; see
# https://github.com/jazzband/django-debug-toolbar/issues/910
DEBUG_TOOLBAR_CONFIG = {
'DISABLE_PANELS': [
'debug_toolbar.panels.redirects.RedirectsPanel',
'debug_toolbar.panels.templates.TemplatesPanel',
],
}
INTERNAL_IPS = ( INTERNAL_IPS = (
'127.0.0.1', '127.0.0.1',
'::1', '::1',

View File

@@ -1,14 +1,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework_swagger.views import get_swagger_view
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.views.static import serve from django.views.static import serve
from rest_framework_swagger.views import get_swagger_view
from netbox.views import APIRootView, HomeView, SearchView from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500
from users.views import LoginView, LogoutView from users.views import LoginView, LogoutView
handler500 = handle_500
swagger_view = get_swagger_view(title='NetBox API') swagger_view = get_swagger_view(title='NetBox API')
_patterns = [ _patterns = [
@@ -29,7 +32,6 @@ _patterns = [
url(r'^secrets/', include('secrets.urls')), url(r'^secrets/', include('secrets.urls')),
url(r'^tenancy/', include('tenancy.urls')), url(r'^tenancy/', include('tenancy.urls')),
url(r'^user/', include('users.urls')), url(r'^user/', include('users.urls')),
url(r'^virtualization/', include('virtualization.urls')),
# API # API
url(r'^api/$', APIRootView.as_view(), name='api-root'), url(r'^api/$', APIRootView.as_view(), name='api-root'),
@@ -39,12 +41,14 @@ _patterns = [
url(r'^api/ipam/', include('ipam.api.urls')), url(r'^api/ipam/', include('ipam.api.urls')),
url(r'^api/secrets/', include('secrets.api.urls')), url(r'^api/secrets/', include('secrets.api.urls')),
url(r'^api/tenancy/', include('tenancy.api.urls')), url(r'^api/tenancy/', include('tenancy.api.urls')),
url(r'^api/virtualization/', include('virtualization.api.urls')),
url(r'^api/docs/', swagger_view, name='api_docs'), url(r'^api/docs/', swagger_view, name='api_docs'),
# Serving static media in Django to pipe it through LoginRequiredMiddleware # Serving static media in Django to pipe it through LoginRequiredMiddleware
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}), url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
# Error testing
url(r'^500/$', trigger_500),
# Admin # Admin
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),

View File

@@ -1,136 +1,118 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
import sys
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.reverse import reverse
from django.shortcuts import render from django.shortcuts import render
from django.views.generic import View from django.views.generic import View
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.views import APIView
from circuits.filters import CircuitFilter, ProviderFilter from circuits.filters import CircuitFilter, ProviderFilter
from circuits.models import Circuit, Provider from circuits.models import Circuit, Provider
from circuits.tables import CircuitTable, ProviderTable from circuits.tables import CircuitSearchTable, ProviderSearchTable
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable from dcim.tables import DeviceSearchTable, DeviceTypeSearchTable, RackSearchTable, SiteSearchTable
from extras.models import TopologyMap, UserAction from extras.models import TopologyMap, UserAction
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable from ipam.tables import AggregateSearchTable, IPAddressSearchTable, PrefixSearchTable, VLANSearchTable, VRFSearchTable
from secrets.filters import SecretFilter from secrets.filters import SecretFilter
from secrets.models import Secret from secrets.models import Secret
from secrets.tables import SecretTable from secrets.tables import SecretSearchTable
from tenancy.filters import TenantFilter from tenancy.filters import TenantFilter
from tenancy.models import Tenant from tenancy.models import Tenant
from tenancy.tables import TenantTable from tenancy.tables import TenantSearchTable
from virtualization.filters import ClusterFilter, VirtualMachineFilter
from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineDetailTable
from .forms import SearchForm from .forms import SearchForm
SEARCH_MAX_RESULTS = 15 SEARCH_MAX_RESULTS = 15
SEARCH_TYPES = OrderedDict(( SEARCH_TYPES = OrderedDict((
# Circuits # Circuits
('provider', { ('provider', {
'queryset': Provider.objects.all(), 'queryset': Provider.objects.all(),
'filter': ProviderFilter, 'filter': ProviderFilter,
'table': ProviderTable, 'table': ProviderSearchTable,
'url': 'circuits:provider_list', 'url': 'circuits:provider_list',
}), }),
('circuit', { ('circuit', {
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'), 'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
'filter': CircuitFilter, 'filter': CircuitFilter,
'table': CircuitTable, 'table': CircuitSearchTable,
'url': 'circuits:circuit_list', 'url': 'circuits:circuit_list',
}), }),
# DCIM # DCIM
('site', { ('site', {
'queryset': Site.objects.select_related('region', 'tenant'), 'queryset': Site.objects.select_related('region', 'tenant'),
'filter': SiteFilter, 'filter': SiteFilter,
'table': SiteTable, 'table': SiteSearchTable,
'url': 'dcim:site_list', 'url': 'dcim:site_list',
}), }),
('rack', { ('rack', {
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'), 'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': RackFilter, 'filter': RackFilter,
'table': RackTable, 'table': RackSearchTable,
'url': 'dcim:rack_list', 'url': 'dcim:rack_list',
}), }),
('devicetype', { ('devicetype', {
'queryset': DeviceType.objects.select_related('manufacturer'), 'queryset': DeviceType.objects.select_related('manufacturer'),
'filter': DeviceTypeFilter, 'filter': DeviceTypeFilter,
'table': DeviceTypeTable, 'table': DeviceTypeSearchTable,
'url': 'dcim:devicetype_list', 'url': 'dcim:devicetype_list',
}), }),
('device', { ('device', {
'queryset': Device.objects.select_related( 'queryset': Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'),
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
),
'filter': DeviceFilter, 'filter': DeviceFilter,
'table': DeviceDetailTable, 'table': DeviceSearchTable,
'url': 'dcim:device_list', 'url': 'dcim:device_list',
}), }),
# IPAM # IPAM
('vrf', { ('vrf', {
'queryset': VRF.objects.select_related('tenant'), 'queryset': VRF.objects.select_related('tenant'),
'filter': VRFFilter, 'filter': VRFFilter,
'table': VRFTable, 'table': VRFSearchTable,
'url': 'ipam:vrf_list', 'url': 'ipam:vrf_list',
}), }),
('aggregate', { ('aggregate', {
'queryset': Aggregate.objects.select_related('rir'), 'queryset': Aggregate.objects.select_related('rir'),
'filter': AggregateFilter, 'filter': AggregateFilter,
'table': AggregateTable, 'table': AggregateSearchTable,
'url': 'ipam:aggregate_list', 'url': 'ipam:aggregate_list',
}), }),
('prefix', { ('prefix', {
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), 'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
'filter': PrefixFilter, 'filter': PrefixFilter,
'table': PrefixTable, 'table': PrefixSearchTable,
'url': 'ipam:prefix_list', 'url': 'ipam:prefix_list',
}), }),
('ipaddress', { ('ipaddress', {
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant'), 'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
'filter': IPAddressFilter, 'filter': IPAddressFilter,
'table': IPAddressTable, 'table': IPAddressSearchTable,
'url': 'ipam:ipaddress_list', 'url': 'ipam:ipaddress_list',
}), }),
('vlan', { ('vlan', {
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'), 'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': VLANFilter, 'filter': VLANFilter,
'table': VLANTable, 'table': VLANSearchTable,
'url': 'ipam:vlan_list', 'url': 'ipam:vlan_list',
}), }),
# Secrets # Secrets
('secret', { ('secret', {
'queryset': Secret.objects.select_related('role', 'device'), 'queryset': Secret.objects.select_related('role', 'device'),
'filter': SecretFilter, 'filter': SecretFilter,
'table': SecretTable, 'table': SecretSearchTable,
'url': 'secrets:secret_list', 'url': 'secrets:secret_list',
}), }),
# Tenancy # Tenancy
('tenant', { ('tenant', {
'queryset': Tenant.objects.select_related('group'), 'queryset': Tenant.objects.select_related('group'),
'filter': TenantFilter, 'filter': TenantFilter,
'table': TenantTable, 'table': TenantSearchTable,
'url': 'tenancy:tenant_list', 'url': 'tenancy:tenant_list',
}), }),
# Virtualization
('cluster', {
'queryset': Cluster.objects.all(),
'filter': ClusterFilter,
'table': ClusterTable,
'url': 'virtualization:cluster_list',
}),
('virtualmachine', {
'queryset': VirtualMachine.objects.select_related(
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
),
'filter': VirtualMachineFilter,
'table': VirtualMachineDetailTable,
'url': 'virtualization:virtualmachine_list',
}),
)) ))
@@ -166,10 +148,6 @@ class HomeView(View):
# Secrets # Secrets
'secret_count': Secret.objects.count(), 'secret_count': Secret.objects.count(),
# Virtualization
'cluster_count': Cluster.objects.count(),
'virtualmachine_count': VirtualMachine.objects.count(),
} }
return render(request, self.template_name, { return render(request, self.template_name, {
@@ -211,7 +189,7 @@ class SearchView(View):
# Construct the results table for this object type # Construct the results table for this object type
filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs
table = table(filtered_queryset, orderable=False) table = table(filtered_queryset)
table.paginate(per_page=SEARCH_MAX_RESULTS) table.paginate(per_page=SEARCH_MAX_RESULTS)
if table.page: if table.page:
@@ -236,12 +214,31 @@ class APIRootView(APIView):
def get(self, request, format=None): def get(self, request, format=None):
return Response(OrderedDict(( return Response({
('circuits', reverse('circuits-api:api-root', request=request, format=format)), 'circuits': reverse('circuits-api:api-root', request=request, format=format),
('dcim', reverse('dcim-api:api-root', request=request, format=format)), 'dcim': reverse('dcim-api:api-root', request=request, format=format),
('extras', reverse('extras-api:api-root', request=request, format=format)), 'extras': reverse('extras-api:api-root', request=request, format=format),
('ipam', reverse('ipam-api:api-root', request=request, format=format)), 'ipam': reverse('ipam-api:api-root', request=request, format=format),
('secrets', reverse('secrets-api:api-root', request=request, format=format)), 'secrets': reverse('secrets-api:api-root', request=request, format=format),
('tenancy', reverse('tenancy-api:api-root', request=request, format=format)), 'tenancy': reverse('tenancy-api:api-root', request=request, format=format),
('virtualization', reverse('virtualization-api:api-root', request=request, format=format)), })
)))
def handle_500(request):
"""
Custom server error handler
"""
type_, error, traceback = sys.exc_info()
return render(request, '500.html', {
'exception': str(type_),
'error': error,
}, status=500)
def trigger_500(request):
"""
Hot-wired method of triggering a server error to test reporting
"""
raise Exception(
"Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are."
)

View File

@@ -1,3 +1,12 @@
"""
WSGI config for do_ipam project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
"""
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application

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