Compare commits

..

42 Commits

Author SHA1 Message Date
Jeremy Stretch
3067c3f262 Merge pull request #1668 from digitalocean/develop
Release v2.2.3
2017-10-31 14:02:15 -04:00
Jeremy Stretch
7a64404299 Merge pull request #1614 from digitalocean/develop
Release v2.2.2
2017-10-17 11:24:02 -04:00
Jeremy Stretch
2bda399982 Merge pull request #1577 from digitalocean/develop
Release v2.2.1
2017-10-12 16:11:17 -04:00
Jeremy Stretch
74731bc6ae Merge pull request #1575 from digitalocean/develop
Release v2.2.0
2017-10-12 14:01:28 -04:00
Jeremy Stretch
7cb287d6c6 Merge pull request #1572 from digitalocean/develop
Release v2.1.6
2017-10-11 13:02:32 -04:00
Jeremy Stretch
aa8f734bd1 Merge pull request #1537 from digitalocean/develop
Release v2.1.5
2017-09-25 14:52:43 -04:00
Jeremy Stretch
f6d1163ddd Merge pull request #1461 from digitalocean/develop
Release v2.1.4
2017-08-30 14:43:01 -04:00
Jeremy Stretch
5be30bd278 Merge pull request #1428 from digitalocean/develop
Release v2.1.3
2017-08-15 15:52:34 -04:00
Jeremy Stretch
fa7b7288c9 Merge pull request #1398 from digitalocean/develop
Release v2.1.2
2017-08-04 10:54:29 -04:00
Jeremy Stretch
9cc03aaa9a Merge pull request #1387 from digitalocean/develop
Release v2.1.1
2017-08-02 14:22:30 -04:00
Jeremy Stretch
1bda56ea23 Merge pull request #1372 from digitalocean/develop
Release v2.1.0
2017-07-25 11:21:44 -04:00
Jeremy Stretch
64a34ced72 Merge pull request #1346 from digitalocean/develop
Release v2.0.10
2017-07-14 10:09:16 -04:00
Jeremy Stretch
e05d379101 Merge pull request #1327 from digitalocean/develop
Release v2.0.9
2017-07-10 09:43:59 -04:00
Jeremy Stretch
a355783377 Merge pull request #1316 from digitalocean/develop
Release v2.0.8
2017-07-05 14:36:08 -04:00
Jeremy Stretch
88239e0b0d Merge pull request #1278 from digitalocean/develop
Release v2.0.7
2017-06-15 14:26:38 -04:00
Jeremy Stretch
5c63a499d5 Merge pull request #1259 from digitalocean/develop
Release v2.0.6
2017-06-12 09:51:15 -04:00
Jeremy Stretch
50496b1a59 Merge pull request #1251 from digitalocean/develop
Release v2.0.5
2017-06-08 10:10:41 -04:00
Jeremy Stretch
f7b0d22f86 Merge pull request #1230 from digitalocean/develop
Release v2.0.4
2017-05-25 14:45:13 -04:00
Jeremy Stretch
ad95b86fdd Merge pull request #1201 from digitalocean/develop
Release v2.0.3
2017-05-18 14:37:19 -04:00
Jeremy Stretch
43e1e0dbc8 Merge pull request #1181 from digitalocean/develop
Release v2.0.2
2017-05-15 13:23:33 -04:00
Jeremy Stretch
f731900e2f Merge pull request #1154 from digitalocean/develop
Release v2.0.1
2017-05-09 22:47:52 -04:00
Jeremy Stretch
b1bcaa33e7 Merge pull request #1148 from digitalocean/develop
Release v2.0.0
2017-05-09 15:09:28 -04:00
Jeremy Stretch
17873706b7 Merge pull request #1094 from digitalocean/develop
Release v1.9.6
2017-04-21 14:52:53 -04:00
Jeremy Stretch
e0ad2b4555 Merge pull request #1054 from digitalocean/develop
Release v1.9.5
2017-04-06 16:35:15 -04:00
Jeremy Stretch
f89d91783b Merge pull request #1035 from digitalocean/develop
Release v1.9.4-r1
2017-04-04 15:50:28 -04:00
Jeremy Stretch
3ffe36e5ed Merge pull request #1032 from digitalocean/develop
Release v1.9.4
2017-04-04 12:01:58 -04:00
Jeremy Stretch
be393a9d10 Merge pull request #989 from digitalocean/develop
Release v1.9.3
2017-03-23 16:27:06 -04:00
Jeremy Stretch
27eefd8705 Merge pull request #966 from digitalocean/develop
Release v1.9.2
2017-03-14 17:14:19 -04:00
Jeremy Stretch
097e0f38ff Merge pull request #949 from digitalocean/develop
Release v1.9.1
2017-03-08 14:40:16 -05:00
Jeremy Stretch
ce26b566a4 Merge pull request #939 from digitalocean/develop
Release v1.9.0-r1
2017-03-03 11:28:02 -05:00
Jeremy Stretch
0e14bc1e02 Merge pull request #933 from digitalocean/develop
Release v1.9.0
2017-03-02 13:27:10 -05:00
Jeremy Stretch
ce6796ed9b Merge pull request #870 from digitalocean/develop
Release v1.8.4
2017-02-03 13:59:02 -05:00
Jeremy Stretch
c90cecc2fb Merge pull request #849 from digitalocean/develop
Release v1.8.3
2017-01-26 13:58:52 -05:00
Jeremy Stretch
b6bbcb0609 Merge pull request #814 from digitalocean/develop
Release v1.8.2
2017-01-18 16:23:28 -05:00
Jeremy Stretch
23f6832d9c Merge pull request #774 from digitalocean/develop
Release v1.8.1
2017-01-04 15:30:54 -05:00
Jeremy Stretch
88dace75a1 Merge pull request #766 from digitalocean/develop
Release v1.8.0
2017-01-03 15:13:36 -05:00
Jeremy Stretch
8eb140fd65 Merge pull request #736 from digitalocean/develop
Release v1.7.3
2016-12-08 12:34:53 -05:00
Jeremy Stretch
1f09f3d096 Merge pull request #728 from digitalocean/develop
Release v1.7.2-r1
2016-12-06 15:38:52 -05:00
Jeremy Stretch
66be85a41f Merge pull request #726 from digitalocean/develop
Release v1.7.2
2016-12-06 14:55:19 -05:00
Jeremy Stretch
814c11167e Merge pull request #694 from digitalocean/develop
Release v1.7.1
2016-11-15 12:34:09 -05:00
Jeremy Stretch
57ddd5086f Merge pull request #666 from digitalocean/develop
Release v1.7.0
2016-11-03 15:12:33 -04:00
Jeremy Stretch
c171547037 Merge pull request #625 from digitalocean/develop
Release v1.6.3
2016-10-19 16:25:50 -04:00
326 changed files with 5069 additions and 15756 deletions

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,16 @@
![NetBox](docs/netbox_logo.png "NetBox logo")
NetBox is an IP address management (IPAM) and data center infrastructure
management (DCIM) tool. Initially conceived by the network engineering team at
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
to address the needs of network and infrastructure engineers.
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss),
or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode.slack.com/)!
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss), or join us on IRC in **#netbox** on **irc.freenode.net**!
### Build Status
NetBox is built against both Python 2.7 and 3.5. Python 3.5 or higher is strongly recommended.
NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
| | status |
|-------------|------------|
@@ -33,12 +27,9 @@ NetBox is built against both Python 2.7 and 3.5. Python 3.5 or higher is strongl
# Installation
Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases)
and run `upgrade.sh`.
Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.
## Alternative Installations
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))

View File

@@ -1,21 +0,0 @@
Django
django-cors-headers
django-debug-toolbar
django-filter==1.1.0
django-mptt
django-tables2
django-taggit
django-timezone-field
djangorestframework==3.8.1
drf-yasg[validation]
graphviz
Markdown
natsort
ncclient
netaddr
paramiko
Pillow
psycopg2-binary
py-gfm
pycryptodome
xmltodict

View File

@@ -5,7 +5,7 @@ Supported HTTP methods:
* `GET`: Retrieve an object or list of objects
* `POST`: Create a new object
* `PUT`: Update an existing object, all mandatory fields must be specified
* `PATCH`: Updates an existing object, only specifying the field to be changed
* `PATCH`: Updates an existing object, only specifiying the field to be changed
* `DELETE`: Delete an existing object
To authenticate a request, attach your token in an `Authorization` header:
@@ -82,15 +82,15 @@ $ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/6
### Creating a new site
Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required. This example includes one non required field, "region."
Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required.
```
$ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site", "region": 5}'
$ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site"}'
{
"id": 16,
"name": "My New Site",
"slug": "my-new-site",
"region": 5,
"region": null,
"tenant": null,
"facility": "",
"asn": null,
@@ -102,7 +102,6 @@ $ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0
"comments": ""
}
```
Note that in this example we are creating a site bound to a region with the ID of 5. For write API actions (`POST`, `PUT`, and `PATCH`) the integer ID value is used for `ForeignKey` (related model) relationships, instead of the nested representation that is used in the `GET` (list) action.
### Modify an existing site
@@ -124,7 +123,7 @@ $ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc
Send an authenticated `DELETE` request to the site detail endpoint.
```
$ curl -v -X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/
$ curl -v X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/
* Connected to localhost (127.0.0.1) port 8000 (#0)
> DELETE /api/dcim/sites/16/ HTTP/1.1
> User-Agent: curl/7.35.0
@@ -144,4 +143,4 @@ $ curl -v -X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f
* Closing connection 0
```
The response to a successful `DELETE` request will have code 204 (No Content); the body of the response will be empty.
The response to a successfull `DELETE` request will have code 204 (No Content); the body of the response will be empty.

View File

@@ -104,7 +104,7 @@ The base serializer is used to represent the default view of a model. This inclu
}
```
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name.
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
@@ -206,28 +206,3 @@ The maximum number of objects that can be returned is limited by the [`MAX_PAGE_
!!! warning
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
# Filtering
A list of objects retrieved via the API can be filtered by passing one or more query parameters. The same parameters used by the web UI work for the API as well. For example, to return only prefixes with a status of "Active" (`1`):
```
GET /api/ipam/prefixes/?status=1
```
The same filter can be incldued multiple times. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
```
GET /api/ipam/prefixes/?status=1&status=2
```
## Custom Fields
To filter on a custom field, prepend `cf_` to the field name. For example, the following query will return only sites where a custom field named `foo` is equal to 123:
```
GET /api/dcim/sites/?cf_foo=123
```
!!! note
Full versus partial matching when filtering is configurable per custom field. Filtering can be toggled (or disabled) for a custom field in the admin UI.

View File

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

View File

@@ -42,8 +42,6 @@ A device type represents a particular hardware model that exists in the real wor
Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type.
A device type can be a parent, child, or neither. Parent devices house child devices in device bays. This relationship is used to model things like blade servers, where child devices function independently but share physical resources like rack space and power. Note that this is **not** intended to model chassis-based devices, wherein child members share a common control plane.
### Manufacturers
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. The model number of a device type must be unique to its manufacturer.
@@ -114,11 +112,3 @@ Console ports connect only to console server ports, and power ports connect only
Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned. Each interface can also be designated as management-only (for out-of-band management) and assigned a short description.
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
---
# Virtual Chassis
A virtual chassis represents a set of devices which share a single control plane: for example, a stack of switches which are managed as a single device. Each device in the virtual chassis is assigned a position and (optionally) a priority. Exactly one device is designated the virtual chassis master: This device will typically be assigned a name, secrets, services, and other attributes related to its management.
It's important to recognize the distinction between a virtual chassis and a chassis-based device. For instance, a virtual chassis is not used to model a chassis switch with removable line cards such as the Juniper EX9208, as its line cards are _not_ physically separate devices capable of operating independently.

View File

@@ -1,9 +1,5 @@
This section entails features of NetBox which are not crucial to its primary functions, but provide additional value.
# Tags
Tags are freeform labels which can be assigned to a variety of objects in NetBox. Tags can be used to categorize and filter objects in addition to built-in and custom fields. Each tag consists of a text label, as well as an auto-generated URL-friendly slug value. Objects can be filtered by the tags assigned to them. Tags can be used across different object types.
# Custom Fields
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
@@ -31,10 +27,6 @@ When a single object is edited, the form will include any custom fields which ha
When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.
# Contextual Configuration Data
Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. (For example, you might want to associate a set of syslog servers for all devices at a particular site.) Context data enables the association of arbitrary data (expressed in JSON format) to devices and virtual machines grouped by region, site, role, platform, and/or tenancy. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
# Export Templates
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
@@ -138,61 +130,3 @@ Certain objects within NetBox (namely sites, racks, and devices) can have photos
!!! note
If you experience a server error while attempting to upload an image attachment, verify that the system user NetBox runs as has write permission to the media root directory (`netbox/media/`).
# Webhooks
A webhook defines an HTTP request that is sent to an external application when certain types of objects are created, updated, and/or deleted in NetBox. When a webhook is triggered, a POST request is sent to its configured URL. This request will include a full representation of the object being modified for consumption by the receiver. Webhooks are configured via the admin UI under Extras > Webhooks.
An optional secret key can be configured for each webhook. This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. This digest can be used by the receiver to authenticate the request's content.
## Requests
The webhook POST request is structured as so (assuming `application/json` as the Content-Type):
```no-highlight
{
"event": "created",
"signal_received_timestamp": 1508769597,
"model": "Site"
"data": {
...
}
}
```
`data` is the serialized representation of the model instance(s) from the event. The same serializers from the NetBox API are used. So an example of the payload for a Site delete event would be:
```no-highlight
{
"event": "deleted",
"signal_received_timestamp": 1508781858.544069,
"model": "Site",
"data": {
"asn": None,
"comments": "",
"contact_email": "",
"contact_name": "",
"contact_phone": "",
"count_circuits": 0,
"count_devices": 0,
"count_prefixes": 0,
"count_racks": 0,
"count_vlans": 0,
"custom_fields": {},
"facility": "",
"id": 54,
"name": "test",
"physical_address": "",
"region": None,
"shipping_address": "",
"slug": "test",
"tenant": None
}
}
```
A request is considered successful if the response status code is any one of a list of "good" statuses defined in the [requests library](https://github.com/requests/requests/blob/205755834d34a8a6ecf2b0b5b2e9c3e6a7f4e4b6/requests/models.py#L688), otherwise the request is marked as having failed. The user may manually retry a failed request.
## Backend Status
Django-rq includes a status page in the admin site which can be used to view the result of processed webhooks and manually retry any failed webhooks. Access it from http://netbox.local/admin/webhook-backend-status/.

View File

@@ -7,13 +7,13 @@ This guide explains how to implement LDAP authentication using an external serve
On Ubuntu:
```no-highlight
sudo apt-get install -y libldap2-dev libsasl2-dev libssl-dev
sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev
```
On CentOS:
```no-highlight
sudo yum install -y openldap-devel
sudo yum install -y python-devel openldap-devel
```
## Install django-auth-ldap
@@ -24,7 +24,7 @@ sudo pip install django-auth-ldap
# Configuration
Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Complete documentation of all `django-auth-ldap` configuration options is included in the project's [official documentation](http://django-auth-ldap.readthedocs.io/).
Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`.
## General Server Configuration
@@ -52,8 +52,6 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
LDAP_IGNORE_CERT_ERRORS = True
```
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
## User Authentication
!!! info
@@ -80,14 +78,14 @@ AUTH_LDAP_USER_ATTR_MAP = {
```
# User Groups for Permissions
!!! info
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. You will also need to modify the import line to use `NestedGroupOfNamesType` instead of `GroupOfNamesType` .
!!! 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
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group
# hierarchy.
# heirarchy.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("dc=example,dc=com", ldap.SCOPE_SUBTREE,
"(objectClass=group)")
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()

View File

@@ -1,8 +1,5 @@
# Migration
!!! warning
Beginning with v2.5, NetBox will no longer support Python 2. It is strongly recommended that you upgrade to Python 3 as soon as possible.
Remove Python 2 packages
```no-highlight

View File

@@ -2,21 +2,43 @@
This section of the documentation discusses installing and configuring the NetBox application.
!!! note
Python 3 is strongly encouraged for new installations. Support for Python 2 will be discontinued in the near future. This documentation includes a guide on [migrating from Python 2 to Python 3](migrating-to-python3).
**Ubuntu**
Python 3:
```no-highlight
# apt-get install -y python3 python3-dev python3-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install3 pip
```
Python 2:
```no-highlight
# apt-get install -y python2.7 python-dev python-setuptools build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# easy_install pip
```
**CentOS**
Python 3:
```no-highlight
# yum install -y epel-release
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install-3.4 pip
```
Python 2:
```no-highlight
# yum install -y epel-release
# yum install -y gcc python2 python-devel python-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install pip
```
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
## Option A: Download a Release
@@ -66,52 +88,33 @@ Resolving deltas: 100% (1495/1495), done.
Checking connectivity... done.
```
!!! warning
Ensure that the media directory (`/opt/netbox/netbox/media/` in this example) and all its subdirectories are writable by the user account as which NetBox runs. If the NetBox process does not have permission to write to this directory, attempts to upload files (e.g. image attachments) will fail. (The appropriate user account will vary by platform.)
`# chown -R netbox:netbox /opt/netbox/netbox/media/`
## Install Python Packages
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
Python 3:
```no-highlight
# pip3 install -r requirements.txt
```
Python 2:
```no-highlight
# pip install -r requirements.txt
```
!!! note
If you encounter errors while installing the required packages, check that you're running a recent version of pip (v9.0.1 or higher) with the command `pip3 -V`.
If you encounter errors while installing the required packages, check that you're running a recent version of pip (v9.0.1 or higher) with the command `pip -V` or `pip3 -V`.
### NAPALM Automation (Optional)
### NAPALM Automation
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:
As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:
```no-highlight
# pip3 install napalm
```
### Webhooks (Optional)
[Webhooks](../data-model/extras/#webhooks) allow NetBox to integrate with external services by pushing out a notification each time a relevant object is created, updated, or deleted. Enabling the webhooks feature requires [Redis](https://redis.io/), a lightweight in-memory database. You may opt to install a Redis sevice locally (see below) or connect to an external one.
**Ubuntu**
```no-highlight
# apt-get install -y redis-server
```
**CentOS**
```no-highlight
# yum install -y redis
```
Enabling webhooks also requires installing the [`django-rq`](https://github.com/ui/django-rq) package. This allows NetBox to use the Redis database as a queue for outgoing webhooks.
```no-highlight
# pip3 install django-rq
```
# Configuration
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
@@ -162,23 +165,11 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
!!! note
In the case of a highly available installation with multiple web servers, `SECRET_KEY` must be identical among all servers in order to maintain a persistent user session state.
## Webhooks Configuration
If you have opted to enable the webhooks, set `WEBHOOKS_ENABLED = True` and define the relevant `REDIS` database parameters. Below is an example:
```python
WEBHOOKS_ENABLED = True
REDIS = {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
}
```
# Run Database Migrations
!!! warning
The examples on the rest of this page call the `python3` executable. Replace this with `python2` or `python` if you're using Python 2.
Before NetBox can run, we need to install the database schema. This is done by running `python3 manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
```no-highlight

View File

@@ -12,37 +12,25 @@ Download and extract the latest version:
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/
# ln -sfn netbox-X.Y.Z/ netbox
# ln -sf netbox-X.Y.Z/ netbox
```
Copy the 'configuration.py' you created when first installing to the new version:
```no-highlight
# cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.py
```
Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.)
```no-highlight
# cp -pr netbox-X.Y.Z/netbox/media/ netbox/netbox/
```
Also make sure to copy over any reports that you've made. Note that if you made them in a separate directory (`/opt/netbox-reports` for example), then you will not need to copy them - the config file that you copied earlier will point to the correct location.
```no-highlight
# cp -r /opt/netbox-X.Y.X/netbox/reports /opt/netbox/netbox/reports/
# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py
```
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
```no-highlight
# cp netbox-X.Y.Z/gunicorn_config.py netbox/gunicorn_config.py
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
```
Copy the LDAP configuration if using LDAP:
```no-highlight
# cp netbox-X.Y.Z/netbox/netbox/ldap_config.py netbox/netbox/netbox/ldap_config.py
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py
```
## Option B: Clone the Git Repository (latest master release)
@@ -65,7 +53,7 @@ Once the new code is in place, run the upgrade script (which may need to be run
```
!!! warning
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below. Note that Python 2 will no longer be supported in NetBox v2.5.
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below.
```no-highlight
# ./upgrade.sh -2
@@ -92,9 +80,3 @@ Finally, restart the WSGI service to run the new code. If you followed this guid
```no-highlight
# sudo supervisorctl restart netbox
```
If using webhooks, also restart the Redis worker:
```no-highlight
# sudo supervisorctl restart netbox-rqworker
```

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,12 @@
from __future__ import unicode_literals
from rest_framework import serializers
from taggit.models import Tag
from circuits.constants import CIRCUIT_STATUS_CHOICES
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, TagField, ValidatedModelSerializer, WritableNestedSerializer
from utilities.api import ValidatedModelSerializer
#
@@ -16,17 +14,16 @@ from utilities.api import ChoiceFieldSerializer, TagField, ValidatedModelSeriali
#
class ProviderSerializer(CustomFieldModelSerializer):
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
class Meta:
model = Provider
fields = [
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
'custom_fields',
]
class NestedProviderSerializer(WritableNestedSerializer):
class NestedProviderSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
class Meta:
@@ -34,6 +31,16 @@ class NestedProviderSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'name', 'slug']
class WritableProviderSerializer(CustomFieldModelSerializer):
class Meta:
model = Provider
fields = [
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
'custom_fields',
]
#
# Circuit types
#
@@ -45,7 +52,7 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug']
class NestedCircuitTypeSerializer(WritableNestedSerializer):
class NestedCircuitTypeSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
class Meta:
@@ -59,20 +66,18 @@ class NestedCircuitTypeSerializer(WritableNestedSerializer):
class CircuitSerializer(CustomFieldModelSerializer):
provider = NestedProviderSerializer()
status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
tenant = NestedTenantSerializer()
class Meta:
model = Circuit
fields = [
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
'custom_fields',
]
class NestedCircuitSerializer(WritableNestedSerializer):
class NestedCircuitSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
@@ -80,14 +85,33 @@ class NestedCircuitSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'cid']
class WritableCircuitSerializer(CustomFieldModelSerializer):
class Meta:
model = Circuit
fields = [
'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
'custom_fields',
]
#
# Circuit Terminations
#
class CircuitTerminationSerializer(ValidatedModelSerializer):
class CircuitTerminationSerializer(serializers.ModelSerializer):
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer()
interface = InterfaceSerializer(required=False, allow_null=True)
interface = InterfaceSerializer()
class Meta:
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
]
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
class Meta:
model = CircuitTermination

View File

@@ -1,15 +1,17 @@
from __future__ import unicode_literals
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from django.shortcuts import get_object_or_404
from circuits import filters
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.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers
@@ -19,7 +21,6 @@ from . import serializers
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Circuit, ['status']),
(CircuitTermination, ['term_side']),
)
@@ -28,12 +29,13 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
# Providers
#
class ProviderViewSet(CustomFieldModelViewSet):
class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Provider.objects.all()
serializer_class = serializers.ProviderSerializer
write_serializer_class = serializers.WritableProviderSerializer
filter_class = filters.ProviderFilter
@action(detail=True)
@detail_route()
def graphs(self, request, pk=None):
"""
A convenience method for rendering graphs for a particular provider.
@@ -58,9 +60,10 @@ class CircuitTypeViewSet(ModelViewSet):
# Circuits
#
class CircuitViewSet(CustomFieldModelViewSet):
class CircuitViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
serializer_class = serializers.CircuitSerializer
write_serializer_class = serializers.WritableCircuitSerializer
filter_class = filters.CircuitFilter
@@ -68,7 +71,8 @@ class CircuitViewSet(CustomFieldModelViewSet):
# Circuit Terminations
#
class CircuitTerminationViewSet(ModelViewSet):
class CircuitTerminationViewSet(WritableSerializerMixin, ModelViewSet):
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
serializer_class = serializers.CircuitTerminationSerializer
write_serializer_class = serializers.WritableCircuitTerminationSerializer
filter_class = filters.CircuitTerminationFilter

View File

@@ -9,8 +9,3 @@ class CircuitsConfig(AppConfig):
def ready(self):
import circuits.signals
# register webhook signals
from extras.webhooks import register_signals
from .models import Circuit, Provider
register_signals([Circuit, Provider])

View File

@@ -1,22 +1,6 @@
from __future__ import unicode_literals
# Circuit statuses
CIRCUIT_STATUS_DEPROVISIONING = 0
CIRCUIT_STATUS_ACTIVE = 1
CIRCUIT_STATUS_PLANNED = 2
CIRCUIT_STATUS_PROVISIONING = 3
CIRCUIT_STATUS_OFFLINE = 4
CIRCUIT_STATUS_DECOMMISSIONED = 5
CIRCUIT_STATUS_CHOICES = [
[CIRCUIT_STATUS_PLANNED, 'Planned'],
[CIRCUIT_STATUS_PROVISIONING, 'Provisioning'],
[CIRCUIT_STATUS_ACTIVE, 'Active'],
[CIRCUIT_STATUS_OFFLINE, 'Offline'],
[CIRCUIT_STATUS_DEPROVISIONING, 'Deprovisioning'],
[CIRCUIT_STATUS_DECOMMISSIONED, 'Decommissioned'],
]
# CircuitTermination sides
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'

View File

@@ -1,13 +1,13 @@
from __future__ import unicode_literals
import django_filters
from django.db.models import Q
from dcim.models import Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NumericInFilter
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Provider, Circuit, CircuitTermination, CircuitType
@@ -28,9 +28,6 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Site (slug)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = Provider
@@ -81,16 +78,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Circuit type (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=CIRCUIT_STATUS_CHOICES,
null_value=None
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -106,9 +99,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Site (slug)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = Circuit

View File

@@ -2,17 +2,16 @@ from __future__ import unicode_literals
from django import forms
from django.db.models import Count
from taggit.forms import TagField
from dcim.models import Site, Device, Interface, Rack
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
AnnotatedMultipleChoiceField, APISelect, add_blank_choice, BootstrapMixin, ChainedFieldsMixin,
ChainedModelChoiceField, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField,
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
SmallTextarea, SlugField,
)
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -23,11 +22,10 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderForm(BootstrapMixin, CustomFieldForm):
slug = SlugField()
comments = CommentField()
tags = TagField(required=False)
class Meta:
model = Provider
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags']
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
widgets = {
'noc_contact': SmallTextarea(attrs={'rows': 5}),
'admin_contact': SmallTextarea(attrs={'rows': 5}),
@@ -46,7 +44,7 @@ class ProviderCSVForm(forms.ModelForm):
class Meta:
model = Provider
fields = Provider.csv_headers
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
help_texts = {
'name': 'Provider name',
'asn': '32-bit autonomous system number',
@@ -55,7 +53,7 @@ class ProviderCSVForm(forms.ModelForm):
}
class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
asn = forms.IntegerField(required=False, label='ASN')
account = forms.CharField(max_length=30, required=False, label='Account number')
@@ -92,7 +90,7 @@ class CircuitTypeCSVForm(forms.ModelForm):
class Meta:
model = CircuitType
fields = CircuitType.csv_headers
fields = ['name', 'slug']
help_texts = {
'name': 'Name of circuit type',
}
@@ -104,13 +102,12 @@ class CircuitTypeCSVForm(forms.ModelForm):
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
comments = CommentField()
tags = TagField(required=False)
class Meta:
model = Circuit
fields = [
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
'comments', 'tags',
'cid', 'type', 'provider', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
'comments',
]
help_texts = {
'cid': "Unique circuit ID",
@@ -136,11 +133,6 @@ class CircuitCSVForm(forms.ModelForm):
'invalid_choice': 'Invalid circuit type.'
}
)
status = CSVChoiceField(
choices=CIRCUIT_STATUS_CHOICES,
required=False,
help_text='Operational status'
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
@@ -153,16 +145,13 @@ class CircuitCSVForm(forms.ModelForm):
class Meta:
model = Circuit
fields = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
]
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
status = forms.ChoiceField(choices=add_blank_choice(CIRCUIT_STATUS_CHOICES), required=False, initial='')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
description = forms.CharField(max_length=100, required=False)
@@ -183,16 +172,10 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug'
)
status = AnnotatedMultipleChoiceField(
choices=CIRCUIT_STATUS_CHOICES,
annotate=Circuit.objects.all(),
annotate_field='status',
required=False
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug',
null_label='-- None --'
null_option=(0, 'None')
)
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-02-06 18:48
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0009_unicode_literals'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='status',
field=models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1),
),
]

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,20 @@
from __future__ import unicode_literals
import django_tables2 as tables
from django.utils.safestring import mark_safe
from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
from django.utils.safestring import mark_safe
from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_ACTIONS = """
<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
STATUS_LABEL = """
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
"""
class CircuitTerminationColumn(tables.Column):
@@ -83,11 +77,10 @@ class CircuitTable(BaseTable):
pk = ToggleColumn()
cid = tables.LinkColumn(verbose_name='ID')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
class Meta(BaseTable.Meta):
model = Circuit
fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'termination_a', 'termination_z', 'description')
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'termination_a', 'termination_z', 'description')

View File

@@ -1,17 +1,16 @@
from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from django.contrib.auth.models import User
from django.urls import reverse
from dcim.models import Site
from extras.constants import GRAPH_TYPE_PROVIDER
from extras.models import Graph
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
from users.models import Token
from utilities.testing import HttpStatusMixin
from utilities.tests import HttpStatusMixin
class ProviderTest(HttpStatusMixin, APITestCase):
@@ -69,7 +68,7 @@ class ProviderTest(HttpStatusMixin, APITestCase):
}
url = reverse('circuits-api:provider-list')
response = self.client.post(url, data, format='json', **self.header)
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Provider.objects.count(), 4)
@@ -77,32 +76,6 @@ class ProviderTest(HttpStatusMixin, APITestCase):
self.assertEqual(provider4.name, data['name'])
self.assertEqual(provider4.slug, data['slug'])
def test_create_provider_bulk(self):
data = [
{
'name': 'Test Provider 4',
'slug': 'test-provider-4',
},
{
'name': 'Test Provider 5',
'slug': 'test-provider-5',
},
{
'name': 'Test Provider 6',
'slug': 'test-provider-6',
},
]
url = reverse('circuits-api:provider-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Provider.objects.count(), 6)
self.assertEqual(response.data[0]['name'], data[0]['name'])
self.assertEqual(response.data[1]['name'], data[1]['name'])
self.assertEqual(response.data[2]['name'], data[2]['name'])
def test_update_provider(self):
data = {
@@ -111,7 +84,7 @@ class ProviderTest(HttpStatusMixin, APITestCase):
}
url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
response = self.client.put(url, data, format='json', **self.header)
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(Provider.objects.count(), 3)
@@ -162,7 +135,7 @@ class CircuitTypeTest(HttpStatusMixin, APITestCase):
}
url = reverse('circuits-api:circuittype-list')
response = self.client.post(url, data, format='json', **self.header)
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(CircuitType.objects.count(), 4)
@@ -178,7 +151,7 @@ class CircuitTypeTest(HttpStatusMixin, APITestCase):
}
url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
response = self.client.put(url, data, format='json', **self.header)
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(CircuitType.objects.count(), 3)
@@ -231,11 +204,10 @@ class CircuitTest(HttpStatusMixin, APITestCase):
'cid': 'TEST0004',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
}
url = reverse('circuits-api:circuit-list')
response = self.client.post(url, data, format='json', **self.header)
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Circuit.objects.count(), 4)
@@ -244,38 +216,6 @@ class CircuitTest(HttpStatusMixin, APITestCase):
self.assertEqual(circuit4.provider_id, data['provider'])
self.assertEqual(circuit4.type_id, data['type'])
def test_create_circuit_bulk(self):
data = [
{
'cid': 'TEST0004',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
},
{
'cid': 'TEST0005',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
},
{
'cid': 'TEST0006',
'provider': self.provider1.pk,
'type': self.circuittype1.pk,
'status': CIRCUIT_STATUS_ACTIVE,
},
]
url = reverse('circuits-api:circuit-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Circuit.objects.count(), 6)
self.assertEqual(response.data[0]['cid'], data[0]['cid'])
self.assertEqual(response.data[1]['cid'], data[1]['cid'])
self.assertEqual(response.data[2]['cid'], data[2]['cid'])
def test_update_circuit(self):
data = {
@@ -285,7 +225,7 @@ class CircuitTest(HttpStatusMixin, APITestCase):
}
url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
response = self.client.put(url, data, format='json', **self.header)
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(Circuit.objects.count(), 3)
@@ -352,7 +292,7 @@ class CircuitTerminationTest(HttpStatusMixin, APITestCase):
}
url = reverse('circuits-api:circuittermination-list')
response = self.client.post(url, data, format='json', **self.header)
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(CircuitTermination.objects.count(), 4)
@@ -372,7 +312,7 @@ class CircuitTerminationTest(HttpStatusMixin, APITestCase):
}
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
response = self.client.put(url, data, format='json', **self.header)
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(CircuitTermination.objects.count(), 3)

View File

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

View File

@@ -6,6 +6,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.generic import View
from extras.models import Graph, GRAPH_TYPE_PROVIDER
@@ -14,8 +15,7 @@ from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .constants import TERM_SIDE_A, TERM_SIDE_Z
from .models import Circuit, CircuitTermination, CircuitType, Provider
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
#
@@ -105,7 +105,9 @@ class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittype'
model = CircuitType
model_form = forms.CircuitTypeForm
default_return_url = 'circuits:circuittype_list'
def get_return_url(self, request, obj):
return reverse('circuits:circuittype_list')
class CircuitTypeEditView(CircuitTypeCreateView):

View File

@@ -1,28 +1,21 @@
from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from taggit.models import Tag
from ipam.models import IPAddress
from circuits.models import Circuit, CircuitTermination
from dcim.constants import (
CONNECTION_STATUS_CHOICES, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_MODE_CHOICES, IFACE_ORDERING_CHOICES,
RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
)
from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis,
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
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 ipam.models import IPAddress, VLAN
from tenancy.api.serializers import NestedTenantSerializer
from users.api.serializers import NestedUserSerializer
from utilities.api import (
ChoiceFieldSerializer, SerializedPKRelatedField, TagField, TimeZoneField, ValidatedModelSerializer,
WritableNestedSerializer,
)
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.models import Cluster
@@ -30,7 +23,7 @@ from virtualization.models import Cluster
# Regions
#
class NestedRegionSerializer(WritableNestedSerializer):
class NestedRegionSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
class Meta:
@@ -39,7 +32,14 @@ class NestedRegionSerializer(WritableNestedSerializer):
class RegionSerializer(serializers.ModelSerializer):
parent = NestedRegionSerializer(required=False, allow_null=True)
parent = NestedRegionSerializer()
class Meta:
model = Region
fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(ValidatedModelSerializer):
class Meta:
model = Region
@@ -51,23 +51,19 @@ class RegionSerializer(serializers.ModelSerializer):
#
class SiteSerializer(CustomFieldModelSerializer):
status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES, required=False)
region = NestedRegionSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
region = NestedRegionSerializer()
tenant = NestedTenantSerializer()
class Meta:
model = Site
fields = [
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes',
'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
'count_vlans', 'count_racks', 'count_devices', 'count_circuits',
]
class NestedSiteSerializer(WritableNestedSerializer):
class NestedSiteSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
class Meta:
@@ -75,11 +71,21 @@ class NestedSiteSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'name', 'slug']
class WritableSiteSerializer(CustomFieldModelSerializer):
class Meta:
model = Site
fields = [
'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields',
]
#
# Rack groups
#
class RackGroupSerializer(ValidatedModelSerializer):
class RackGroupSerializer(serializers.ModelSerializer):
site = NestedSiteSerializer()
class Meta:
@@ -87,7 +93,7 @@ class RackGroupSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'site']
class NestedRackGroupSerializer(WritableNestedSerializer):
class NestedRackGroupSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
class Meta:
@@ -95,6 +101,13 @@ class NestedRackGroupSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(ValidatedModelSerializer):
class Meta:
model = RackGroup
fields = ['id', 'name', 'slug', 'site']
#
# Rack roles
#
@@ -106,7 +119,7 @@ class RackRoleSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'color']
class NestedRackRoleSerializer(WritableNestedSerializer):
class NestedRackRoleSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
class Meta:
@@ -120,40 +133,21 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
class RackSerializer(CustomFieldModelSerializer):
site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
role = NestedRackRoleSerializer(required=False, allow_null=True)
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False)
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
group = NestedRackGroupSerializer()
tenant = NestedTenantSerializer()
role = NestedRackRoleSerializer()
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES)
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
class Meta:
model = Rack
fields = [
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
# prevents facility_id from being interpreted as a required field.
validators = [
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'name'))
'u_height', 'desc_units', 'comments', 'custom_fields',
]
def validate(self, data):
# Validate uniqueness of (group, facility_id) since we omitted the automatically-created validator from Meta.
if data.get('facility_id', None):
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'facility_id'))
validator.set_context(self)
validator(data)
# Enforce model validation
super(RackSerializer, self).validate(data)
return data
class NestedRackSerializer(WritableNestedSerializer):
class NestedRackSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
class Meta:
@@ -161,11 +155,39 @@ class NestedRackSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'name', 'display_name']
class WritableRackSerializer(CustomFieldModelSerializer):
class Meta:
model = Rack
fields = [
'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width', 'u_height',
'desc_units', 'comments', 'custom_fields',
]
# Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
# prevents facility_id from being interpreted as a required field.
validators = [
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('site', 'name'))
]
def validate(self, data):
# Validate uniqueness of (site, facility_id) since we omitted the automatically-created validator from Meta.
if data.get('facility_id', None):
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('site', 'facility_id'))
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableRackSerializer, self).validate(data)
return data
#
# Rack units
#
class NestedDeviceSerializer(WritableNestedSerializer):
class NestedDeviceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
class Meta:
@@ -187,14 +209,19 @@ class RackUnitSerializer(serializers.Serializer):
# Rack reservations
#
class RackReservationSerializer(ValidatedModelSerializer):
class RackReservationSerializer(serializers.ModelSerializer):
rack = NestedRackSerializer()
user = NestedUserSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
class Meta:
model = RackReservation
fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(ValidatedModelSerializer):
class Meta:
model = RackReservation
fields = ['id', 'rack', 'units', 'user', 'description']
#
@@ -208,7 +235,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug']
class NestedManufacturerSerializer(WritableNestedSerializer):
class NestedManufacturerSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
class Meta:
@@ -222,34 +249,43 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
class DeviceTypeSerializer(CustomFieldModelSerializer):
manufacturer = NestedManufacturerSerializer()
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES, required=False)
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES, required=False)
interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES)
subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES)
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
class Meta:
model = DeviceType
fields = [
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'instance_count',
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
'instance_count',
]
class NestedDeviceTypeSerializer(WritableNestedSerializer):
class NestedDeviceTypeSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
manufacturer = NestedManufacturerSerializer()
class Meta:
model = DeviceType
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
class WritableDeviceTypeSerializer(CustomFieldModelSerializer):
class Meta:
model = DeviceType
fields = [
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
]
#
# Console port templates
#
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class ConsolePortTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
@@ -257,11 +293,18 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsolePortTemplate
fields = ['id', 'device_type', 'name']
#
# Console server port templates
#
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
@@ -269,11 +312,18 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPortTemplate
fields = ['id', 'device_type', 'name']
#
# Power port templates
#
class PowerPortTemplateSerializer(ValidatedModelSerializer):
class PowerPortTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
@@ -281,11 +331,18 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPortTemplate
fields = ['id', 'device_type', 'name']
#
# Power outlet templates
#
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class PowerOutletTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
@@ -293,13 +350,27 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutletTemplate
fields = ['id', 'device_type', 'name']
#
# Interface templates
#
class InterfaceTemplateSerializer(ValidatedModelSerializer):
class InterfaceTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
class Meta:
model = InterfaceTemplate
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = InterfaceTemplate
@@ -310,7 +381,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
# Device bay templates
#
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
class DeviceBayTemplateSerializer(serializers.ModelSerializer):
device_type = NestedDeviceTypeSerializer()
class Meta:
@@ -318,6 +389,13 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceBayTemplate
fields = ['id', 'device_type', 'name']
#
# Device roles
#
@@ -329,7 +407,7 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'color', 'vm_role']
class NestedDeviceRoleSerializer(WritableNestedSerializer):
class NestedDeviceRoleSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
class Meta:
@@ -342,14 +420,13 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
#
class PlatformSerializer(ValidatedModelSerializer):
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
class Meta:
model = Platform
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'rpc_client']
fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client']
class NestedPlatformSerializer(WritableNestedSerializer):
class NestedPlatformSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
class Meta:
@@ -379,55 +456,28 @@ class NestedClusterSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name']
# Cannot import NestedVirtualChassisSerializer due to circular dependency
class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer()
class Meta:
model = VirtualChassis
fields = ['id', 'url', 'master']
class DeviceSerializer(CustomFieldModelSerializer):
device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
platform = NestedPlatformSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer()
platform = NestedPlatformSerializer()
site = NestedSiteSerializer()
rack = NestedRackSerializer(required=False, allow_null=True)
face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES, required=False)
status = ChoiceFieldSerializer(choices=DEVICE_STATUS_CHOICES, required=False)
primary_ip = DeviceIPAddressSerializer(read_only=True)
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = DeviceIPAddressSerializer(required=False, allow_null=True)
rack = NestedRackSerializer()
face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES)
status = ChoiceFieldSerializer(choices=STATUS_CHOICES)
primary_ip = DeviceIPAddressSerializer()
primary_ip4 = DeviceIPAddressSerializer()
primary_ip6 = DeviceIPAddressSerializer()
parent_device = serializers.SerializerMethodField()
cluster = NestedClusterSerializer(required=False, allow_null=True)
virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
cluster = NestedClusterSerializer()
class Meta:
model = Device
fields = [
'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',
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
'cluster', 'comments', 'custom_fields',
]
validators = []
def validate(self, data):
# Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta.
if data.get('rack') and data.get('position') and data.get('face'):
validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face'))
validator.set_context(self)
validator(data)
# Enforce model validation
super(DeviceSerializer, self).validate(data)
return data
def get_parent_device(self, obj):
try:
@@ -440,106 +490,120 @@ class DeviceSerializer(CustomFieldModelSerializer):
return data
class DeviceWithConfigContextSerializer(DeviceSerializer):
config_context = serializers.SerializerMethodField()
class WritableDeviceSerializer(CustomFieldModelSerializer):
class Meta(DeviceSerializer.Meta):
class Meta:
model = Device
fields = [
'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',
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
'config_context', 'created', 'last_updated',
'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack',
'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'cluster', 'comments', 'custom_fields',
]
validators = []
def get_config_context(self, obj):
return obj.get_config_context()
def validate(self, data):
# Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta.
if data.get('rack') and data.get('position') and data.get('face'):
validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face'))
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableDeviceSerializer, self).validate(data)
return data
#
# Console server ports
#
class ConsoleServerPortSerializer(ValidatedModelSerializer):
class ConsoleServerPortSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
class Meta:
model = ConsoleServerPort
fields = ['id', 'device', 'name', 'connected_console', 'tags']
fields = ['id', 'device', 'name', 'connected_console']
read_only_fields = ['connected_console']
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True)
class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPort
fields = ['id', 'url', 'device', 'name']
fields = ['id', 'device', 'name']
#
# Console ports
#
class ConsolePortSerializer(ValidatedModelSerializer):
class ConsolePortSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
cs_port = NestedConsoleServerPortSerializer(required=False, allow_null=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
cs_port = ConsoleServerPortSerializer()
class Meta:
model = ConsolePort
fields = ['id', 'device', 'name', 'cs_port', 'connection_status', 'tags']
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
class WritableConsolePortSerializer(ValidatedModelSerializer):
class Meta:
model = ConsolePort
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
#
# Power outlets
#
class PowerOutletSerializer(ValidatedModelSerializer):
class PowerOutletSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
class Meta:
model = PowerOutlet
fields = ['id', 'device', 'name', 'connected_port', 'tags']
fields = ['id', 'device', 'name', 'connected_port']
read_only_fields = ['connected_port']
class NestedPowerOutletSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer(read_only=True)
class WritablePowerOutletSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutlet
fields = ['id', 'url', 'device', 'name']
fields = ['id', 'device', 'name']
#
# Power ports
#
class PowerPortSerializer(ValidatedModelSerializer):
class PowerPortSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
power_outlet = NestedPowerOutletSerializer(required=False, allow_null=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
power_outlet = PowerOutletSerializer()
class Meta:
model = PowerPort
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status', 'tags']
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
class WritablePowerPortSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPort
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
#
# Interfaces
#
class NestedInterfaceSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
class NestedInterfaceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
class Meta:
model = Interface
fields = ['id', 'url', 'device', 'name']
fields = ['id', 'url', 'name']
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
@@ -550,8 +614,8 @@ class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'cid']
class InterfaceCircuitTerminationSerializer(WritableNestedSerializer):
circuit = InterfaceNestedCircuitSerializer(read_only=True)
class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
circuit = InterfaceNestedCircuitSerializer()
class Meta:
model = CircuitTermination
@@ -560,59 +624,21 @@ class InterfaceCircuitTerminationSerializer(WritableNestedSerializer):
]
# Cannot import ipam.api.NestedVLANSerializer due to circular dependency
class InterfaceVLANSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
class Meta:
model = VLAN
fields = ['id', 'url', 'vid', 'name', 'display_name']
class InterfaceSerializer(ValidatedModelSerializer):
class InterfaceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
is_connected = serializers.SerializerMethodField(read_only=True)
interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES, required=False)
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(),
serializer=InterfaceVLANSerializer,
required=False,
many=True
)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
circuit_termination = InterfaceCircuitTerminationSerializer()
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans',
'tags',
'is_connected', 'interface_connection', 'circuit_termination',
]
def validate(self, data):
# All associated VLANs be global or assigned to the parent device's site.
device = self.instance.device if self.instance else data.get('device')
untagged_vlan = data.get('untagged_vlan')
if untagged_vlan and untagged_vlan.site not in [device.site, None]:
raise serializers.ValidationError({
'untagged_vlan': "VLAN {} must belong to the same site as the interface's parent device, or it must be "
"global.".format(untagged_vlan)
})
for vlan in data.get('tagged_vlans', []):
if vlan.site not in [device.site, None]:
raise serializers.ValidationError({
'tagged_vlans': "VLAN {} must belong to the same site as the interface's parent device, or it must "
"be global.".format(vlan)
})
return super(InterfaceSerializer, self).validate(data)
def get_is_connected(self, obj):
"""
Return True if the interface has a connected interface or circuit termination.
@@ -628,29 +654,50 @@ class InterfaceSerializer(ValidatedModelSerializer):
def get_interface_connection(self, obj):
if obj.connection:
context = {
'request': self.context['request'],
'interface': obj.connected_interface,
}
return ContextualInterfaceConnectionSerializer(obj.connection, context=context).data
return OrderedDict((
('interface', PeerInterfaceSerializer(obj.connected_interface, context=self.context).data),
('status', obj.connection.connection_status),
))
return None
class PeerInterfaceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
class Meta:
model = Interface
fields = [
'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'description',
]
class WritableInterfaceSerializer(ValidatedModelSerializer):
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
]
#
# Device bays
#
class DeviceBaySerializer(ValidatedModelSerializer):
class DeviceBaySerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
installed_device = NestedDeviceSerializer()
class Meta:
model = DeviceBay
fields = ['id', 'device', 'name', 'installed_device', 'tags']
fields = ['id', 'device', 'name', 'installed_device']
class NestedDeviceBaySerializer(WritableNestedSerializer):
class NestedDeviceBaySerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
class Meta:
@@ -658,22 +705,36 @@ class NestedDeviceBaySerializer(WritableNestedSerializer):
fields = ['id', 'url', 'name']
class WritableDeviceBaySerializer(ValidatedModelSerializer):
class Meta:
model = DeviceBay
fields = ['id', 'device', 'name', 'installed_device']
#
# Inventory items
#
class InventoryItemSerializer(ValidatedModelSerializer):
class InventoryItemSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
# Provide a default value to satisfy UniqueTogetherValidator
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
manufacturer = NestedManufacturerSerializer()
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
class Meta:
model = InventoryItem
fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags',
'description',
]
class WritableInventoryItemSerializer(ValidatedModelSerializer):
class Meta:
model = InventoryItem
fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
]
@@ -681,17 +742,17 @@ class InventoryItemSerializer(ValidatedModelSerializer):
# Interface connections
#
class InterfaceConnectionSerializer(ValidatedModelSerializer):
interface_a = NestedInterfaceSerializer()
interface_b = NestedInterfaceSerializer()
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES, required=False)
class InterfaceConnectionSerializer(serializers.ModelSerializer):
interface_a = PeerInterfaceSerializer()
interface_b = PeerInterfaceSerializer()
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES)
class Meta:
model = InterfaceConnection
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
class NestedInterfaceConnectionSerializer(WritableNestedSerializer):
class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
class Meta:
@@ -699,37 +760,8 @@ class NestedInterfaceConnectionSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'connection_status']
class ContextualInterfaceConnectionSerializer(serializers.ModelSerializer):
"""
A read-only representation of an InterfaceConnection from the perspective of either of its two connected Interfaces.
"""
interface = serializers.SerializerMethodField(read_only=True)
connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
class Meta:
model = InterfaceConnection
fields = ['id', 'interface', 'connection_status']
def get_interface(self, obj):
return NestedInterfaceSerializer(self.context['interface'], context=self.context).data
#
# Virtual chassis
#
class VirtualChassisSerializer(ValidatedModelSerializer):
master = NestedDeviceSerializer()
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
class Meta:
model = VirtualChassis
fields = ['id', 'master', 'domain', 'tags']
class NestedVirtualChassisSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
class Meta:
model = VirtualChassis
fields = ['id', 'url']
fields = ['id', 'interface_a', 'interface_b', 'connection_status']

View File

@@ -60,9 +60,6 @@ router.register(r'console-connections', views.ConsoleConnectionViewSet, base_nam
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
router.register(r'interface-connections', views.InterfaceConnectionViewSet)
# Virtual chassis
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
# Miscellaneous
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')

View File

@@ -1,31 +1,28 @@
from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
from django.conf import settings
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from drf_yasg import openapi
from drf_yasg.openapi import Parameter
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.mixins import ListModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ViewSet
from dcim import filters
from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis,
RackReservation, RackRole, Region, Site,
)
from dcim import filters
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable
from . import serializers
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin
from .exceptions import MissingFilterException
from . import serializers
#
@@ -36,12 +33,11 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Device, ['face', 'status']),
(ConsolePort, ['connection_status']),
(Interface, ['form_factor', 'mode']),
(Interface, ['form_factor']),
(InterfaceConnection, ['connection_status']),
(InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']),
(Rack, ['type', 'width']),
(Site, ['status']),
)
@@ -49,9 +45,10 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
# Regions
#
class RegionViewSet(ModelViewSet):
class RegionViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
write_serializer_class = serializers.WritableRegionSerializer
filter_class = filters.RegionFilter
@@ -59,12 +56,13 @@ class RegionViewSet(ModelViewSet):
# Sites
#
class SiteViewSet(CustomFieldModelViewSet):
class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Site.objects.select_related('region', 'tenant')
serializer_class = serializers.SiteSerializer
write_serializer_class = serializers.WritableSiteSerializer
filter_class = filters.SiteFilter
@action(detail=True)
@detail_route()
def graphs(self, request, pk=None):
"""
A convenience method for rendering graphs for a particular site.
@@ -79,9 +77,10 @@ class SiteViewSet(CustomFieldModelViewSet):
# Rack groups
#
class RackGroupViewSet(ModelViewSet):
class RackGroupViewSet(WritableSerializerMixin, ModelViewSet):
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
write_serializer_class = serializers.WritableRackGroupSerializer
filter_class = filters.RackGroupFilter
@@ -99,12 +98,13 @@ class RackRoleViewSet(ModelViewSet):
# Racks
#
class RackViewSet(CustomFieldModelViewSet):
class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
serializer_class = serializers.RackSerializer
write_serializer_class = serializers.WritableRackSerializer
filter_class = filters.RackFilter
@action(detail=True)
@detail_route()
def units(self, request, pk=None):
"""
List rack units (by rack)
@@ -129,9 +129,10 @@ class RackViewSet(CustomFieldModelViewSet):
# Rack reservations
#
class RackReservationViewSet(ModelViewSet):
queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
class RackReservationViewSet(WritableSerializerMixin, ModelViewSet):
queryset = RackReservation.objects.select_related('rack')
serializer_class = serializers.RackReservationSerializer
write_serializer_class = serializers.WritableRackReservationSerializer
filter_class = filters.RackReservationFilter
# Assign user from request
@@ -153,9 +154,10 @@ class ManufacturerViewSet(ModelViewSet):
# Device types
#
class DeviceTypeViewSet(CustomFieldModelViewSet):
class DeviceTypeViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = DeviceType.objects.select_related('manufacturer')
serializer_class = serializers.DeviceTypeSerializer
write_serializer_class = serializers.WritableDeviceTypeSerializer
filter_class = filters.DeviceTypeFilter
@@ -163,39 +165,45 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
# Device type components
#
class ConsolePortTemplateViewSet(ModelViewSet):
class ConsolePortTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.ConsolePortTemplateSerializer
write_serializer_class = serializers.WritableConsolePortTemplateSerializer
filter_class = filters.ConsolePortTemplateFilter
class ConsoleServerPortTemplateViewSet(ModelViewSet):
class ConsoleServerPortTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.ConsoleServerPortTemplateSerializer
write_serializer_class = serializers.WritableConsoleServerPortTemplateSerializer
filter_class = filters.ConsoleServerPortTemplateFilter
class PowerPortTemplateViewSet(ModelViewSet):
class PowerPortTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.PowerPortTemplateSerializer
write_serializer_class = serializers.WritablePowerPortTemplateSerializer
filter_class = filters.PowerPortTemplateFilter
class PowerOutletTemplateViewSet(ModelViewSet):
class PowerOutletTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.PowerOutletTemplateSerializer
write_serializer_class = serializers.WritablePowerOutletTemplateSerializer
filter_class = filters.PowerOutletTemplateFilter
class InterfaceTemplateViewSet(ModelViewSet):
class InterfaceTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.InterfaceTemplateSerializer
write_serializer_class = serializers.WritableInterfaceTemplateSerializer
filter_class = filters.InterfaceTemplateFilter
class DeviceBayTemplateViewSet(ModelViewSet):
class DeviceBayTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer
write_serializer_class = serializers.WritableDeviceBayTemplateSerializer
filter_class = filters.DeviceBayTemplateFilter
@@ -223,24 +231,17 @@ class PlatformViewSet(ModelViewSet):
# Devices
#
class DeviceViewSet(CustomFieldModelViewSet):
class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Device.objects.select_related(
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
'virtual_chassis__master',
).prefetch_related(
'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
)
serializer_class = serializers.DeviceSerializer
write_serializer_class = serializers.WritableDeviceSerializer
filter_class = filters.DeviceFilter
def get_serializer_class(self):
"""
Include rendered config context when retrieving a single Device.
"""
if self.action == 'retrieve':
return serializers.DeviceWithConfigContextSerializer
return serializers.DeviceSerializer
@action(detail=True, url_path='napalm')
@detail_route(url_path='napalm')
def napalm(self, request, pk):
"""
Execute a NAPALM method on a Device
@@ -255,14 +256,12 @@ class DeviceViewSet(CustomFieldModelViewSet):
device.platform
))
# Check that NAPALM is installed
# Check that NAPALM is installed and verify the configured driver
try:
import napalm
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
except ImportError:
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
# Validate the configured driver
try:
driver = napalm.get_network_driver(device.platform.napalm_driver)
except ModuleImportError:
@@ -286,15 +285,12 @@ class DeviceViewSet(CustomFieldModelViewSet):
# TODO: Improve error handling
response = OrderedDict([(m, None) for m in napalm_methods])
ip_address = str(device.primary_ip.address.ip)
optional_args = settings.NAPALM_ARGS.copy()
if device.platform.napalm_args is not None:
optional_args.update(device.platform.napalm_args)
d = driver(
hostname=ip_address,
username=settings.NAPALM_USERNAME,
password=settings.NAPALM_PASSWORD,
timeout=settings.NAPALM_TIMEOUT,
optional_args=optional_args
optional_args=settings.NAPALM_ARGS
)
try:
d.open()
@@ -311,36 +307,41 @@ class DeviceViewSet(CustomFieldModelViewSet):
# Device components
#
class ConsolePortViewSet(ModelViewSet):
class ConsolePortViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
serializer_class = serializers.ConsolePortSerializer
write_serializer_class = serializers.WritableConsolePortSerializer
filter_class = filters.ConsolePortFilter
class ConsoleServerPortViewSet(ModelViewSet):
class ConsoleServerPortViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
serializer_class = serializers.ConsoleServerPortSerializer
write_serializer_class = serializers.WritableConsoleServerPortSerializer
filter_class = filters.ConsoleServerPortFilter
class PowerPortViewSet(ModelViewSet):
class PowerPortViewSet(WritableSerializerMixin, ModelViewSet):
queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
serializer_class = serializers.PowerPortSerializer
write_serializer_class = serializers.WritablePowerPortSerializer
filter_class = filters.PowerPortFilter
class PowerOutletViewSet(ModelViewSet):
class PowerOutletViewSet(WritableSerializerMixin, ModelViewSet):
queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
serializer_class = serializers.PowerOutletSerializer
write_serializer_class = serializers.WritablePowerOutletSerializer
filter_class = filters.PowerOutletFilter
class InterfaceViewSet(ModelViewSet):
class InterfaceViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Interface.objects.select_related('device')
serializer_class = serializers.InterfaceSerializer
write_serializer_class = serializers.WritableInterfaceSerializer
filter_class = filters.InterfaceFilter
@action(detail=True)
@detail_route()
def graphs(self, request, pk=None):
"""
A convenience method for rendering graphs for a particular interface.
@@ -351,15 +352,17 @@ class InterfaceViewSet(ModelViewSet):
return Response(serializer.data)
class DeviceBayViewSet(ModelViewSet):
class DeviceBayViewSet(WritableSerializerMixin, ModelViewSet):
queryset = DeviceBay.objects.select_related('installed_device')
serializer_class = serializers.DeviceBaySerializer
write_serializer_class = serializers.WritableDeviceBaySerializer
filter_class = filters.DeviceBayFilter
class InventoryItemViewSet(ModelViewSet):
class InventoryItemViewSet(WritableSerializerMixin, ModelViewSet):
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
serializer_class = serializers.InventoryItemSerializer
write_serializer_class = serializers.WritableInventoryItemSerializer
filter_class = filters.InventoryItemFilter
@@ -379,21 +382,13 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
filter_class = filters.PowerConnectionFilter
class InterfaceConnectionViewSet(ModelViewSet):
class InterfaceConnectionViewSet(WritableSerializerMixin, ModelViewSet):
queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
serializer_class = serializers.InterfaceConnectionSerializer
write_serializer_class = serializers.WritableInterfaceConnectionSerializer
filter_class = filters.InterfaceConnectionFilter
#
# Virtual chassis
#
class VirtualChassisViewSet(ModelViewSet):
queryset = VirtualChassis.objects.all()
serializer_class = serializers.VirtualChassisSerializer
#
# Miscellaneous
#
@@ -408,20 +403,14 @@ class ConnectedDeviceViewSet(ViewSet):
* `peer-interface`: The name of the peer interface
"""
permission_classes = [IsAuthenticatedOrLoginNotRequired]
_device_param = Parameter('peer-device', 'query',
description='The name of the peer device', required=True, type=openapi.TYPE_STRING)
_interface_param = Parameter('peer-interface', 'query',
description='The name of the peer interface', required=True, type=openapi.TYPE_STRING)
def get_view_name(self):
return "Connected Device Locator"
@swagger_auto_schema(
manual_parameters=[_device_param, _interface_param], responses={'200': serializers.DeviceSerializer})
def list(self, request):
peer_device_name = request.query_params.get(self._device_param.name)
peer_interface_name = request.query_params.get(self._interface_param.name)
peer_device_name = request.query_params.get('peer-device')
peer_interface_name = request.query_params.get('peer-interface')
if not peer_device_name or not peer_interface_name:
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')

View File

@@ -6,12 +6,3 @@ from django.apps import AppConfig
class DCIMConfig(AppConfig):
name = "dcim"
verbose_name = "DCIM"
def ready(self):
import dcim.signals
# register webhook signals
from extras.webhooks import register_signals
from .models import Site, Rack, RackGroup, Device, Interface
register_signals([Site, Rack, Device, Interface, RackGroup])

View File

@@ -193,43 +193,24 @@ WIRELESS_IFACE_TYPES = [
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
IFACE_MODE_ACCESS = 100
IFACE_MODE_TAGGED = 200
IFACE_MODE_TAGGED_ALL = 300
IFACE_MODE_CHOICES = [
[IFACE_MODE_ACCESS, 'Access'],
[IFACE_MODE_TAGGED, 'Tagged'],
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
]
# Device statuses
DEVICE_STATUS_OFFLINE = 0
DEVICE_STATUS_ACTIVE = 1
DEVICE_STATUS_PLANNED = 2
DEVICE_STATUS_STAGED = 3
DEVICE_STATUS_FAILED = 4
DEVICE_STATUS_INVENTORY = 5
DEVICE_STATUS_CHOICES = [
[DEVICE_STATUS_ACTIVE, 'Active'],
[DEVICE_STATUS_OFFLINE, 'Offline'],
[DEVICE_STATUS_PLANNED, 'Planned'],
[DEVICE_STATUS_STAGED, 'Staged'],
[DEVICE_STATUS_FAILED, 'Failed'],
[DEVICE_STATUS_INVENTORY, 'Inventory'],
STATUS_OFFLINE = 0
STATUS_ACTIVE = 1
STATUS_PLANNED = 2
STATUS_STAGED = 3
STATUS_FAILED = 4
STATUS_INVENTORY = 5
STATUS_CHOICES = [
[STATUS_ACTIVE, 'Active'],
[STATUS_OFFLINE, 'Offline'],
[STATUS_PLANNED, 'Planned'],
[STATUS_STAGED, 'Staged'],
[STATUS_FAILED, 'Failed'],
[STATUS_INVENTORY, 'Inventory'],
]
# Site statuses
SITE_STATUS_ACTIVE = 1
SITE_STATUS_PLANNED = 2
SITE_STATUS_RETIRED = 4
SITE_STATUS_CHOICES = [
[SITE_STATUS_ACTIVE, 'Active'],
[SITE_STATUS_PLANNED, 'Planned'],
[SITE_STATUS_RETIRED, 'Retired'],
]
# Bootstrap CSS classes for device statuses
STATUS_CLASSES = {
# Bootstrap CSS classes for device stasuses
DEVICE_STATUS_CLASSES = {
0: 'warning',
1: 'success',
2: 'info',

View File

@@ -1,38 +1,31 @@
from __future__ import unicode_literals
import django_filters
from django.contrib.auth.models import User
from django.db.models import Q
from netaddr import EUI
from netaddr.core import AddrFormatError
from django.contrib.auth.models import User
from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableCharFieldFilter, NumericInFilter
from virtualization.models import Cluster
from .constants import (
DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES,
WIRELESS_IFACE_TYPES,
)
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
)
class RegionFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
)
parent = django_filters.ModelMultipleChoiceFilter(
name='parent__slug',
queryset=Region.objects.all(),
to_field_name='slug',
label='Parent region (slug)',
@@ -42,15 +35,6 @@ class RegionFilter(django_filters.FilterSet):
model = Region
fields = ['name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
@@ -58,16 +42,11 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search',
label='Search',
)
status = django_filters.MultipleChoiceFilter(
choices=SITE_STATUS_CHOICES,
null_value=None
)
region_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Region (ID)',
)
region = django_filters.ModelMultipleChoiceFilter(
name='region__slug',
queryset=Region.objects.all(),
to_field_name='slug',
label='Region (slug)',
@@ -77,14 +56,10 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = Site
@@ -96,7 +71,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
qs_filter = (
Q(name__icontains=value) |
Q(facility__icontains=value) |
Q(description__icontains=value) |
Q(physical_address__icontains=value) |
Q(shipping_address__icontains=value) |
Q(contact_name__icontains=value) |
@@ -157,7 +131,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
name='group__slug',
name='group',
queryset=RackGroup.objects.all(),
to_field_name='slug',
label='Group',
@@ -167,7 +141,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -177,18 +151,15 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='role__slug',
name='role',
queryset=RackRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = Rack
fields = ['name', 'serial', 'type', 'width', 'u_height', 'desc_units']
fields = ['serial', 'type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value):
if not value.strip():
@@ -228,21 +199,11 @@ class RackReservationFilter(django_filters.FilterSet):
label='Group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
name='rack__group__slug',
name='rack__group',
queryset=RackGroup.objects.all(),
to_field_name='slug',
label='Group',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
label='User (ID)',
@@ -292,9 +253,6 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Manufacturer (slug)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = DeviceType
@@ -368,21 +326,10 @@ class DeviceRoleFilter(django_filters.FilterSet):
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color', 'vm_role']
fields = ['name', 'slug', 'color']
class PlatformFilter(django_filters.FilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
class Meta:
model = Platform
@@ -426,7 +373,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -436,7 +383,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Platform (ID)',
)
platform = django_filters.ModelMultipleChoiceFilter(
name='platform__slug',
name='platform',
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
@@ -474,8 +421,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Device model (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=DEVICE_STATUS_CHOICES,
null_value=None
choices=STATUS_CHOICES
)
is_full_depth = django_filters.BooleanFilter(
name='device_type__is_full_depth',
@@ -501,18 +447,10 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='_has_primary_ip',
label='Has a primary IP',
)
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
name='virtual_chassis',
queryset=VirtualChassis.objects.all(),
label='Virtual chassis (ID)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = Device
fields = ['serial', 'position']
fields = ['serial']
def search(self, queryset, name, value):
if not value.strip():
@@ -558,9 +496,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='name',
label='Device (name)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class ConsolePortFilter(DeviceComponentFilterSet):
@@ -619,9 +554,6 @@ class InterfaceFilter(django_filters.FilterSet):
method='_mac_address',
label='MAC address',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = Interface
@@ -630,9 +562,8 @@ class InterfaceFilter(django_filters.FilterSet):
def filter_device(self, queryset, name, value):
try:
device = Device.objects.select_related('device_type').get(**{name: value})
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
ordering = device.device_type.interface_ordering
return queryset.filter(pk__in=vc_interface_ids).order_naturally(ordering)
return queryset.filter(device=device).order_naturally(ordering)
except Device.DoesNotExist:
return queryset.none()
@@ -664,10 +595,6 @@ class DeviceBayFilter(DeviceComponentFilterSet):
class InventoryItemFilter(DeviceComponentFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)',
@@ -686,64 +613,7 @@ class InventoryItemFilter(DeviceComponentFilterSet):
class Meta:
model = InventoryItem
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(part_id__icontains=value) |
Q(serial__iexact=value) |
Q(asset_tag__iexact=value) |
Q(description__icontains=value)
)
return queryset.filter(qs_filter)
class VirtualChassisFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='master__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='master__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site name (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='master__tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='master__tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = VirtualChassis
fields = ['domain']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(master__name__icontains=value) |
Q(domain__icontains=value)
)
return queryset.filter(qs_filter)
fields = ['name', 'part_id', 'serial', 'discovered']
class ConsoleConnectionFilter(django_filters.FilterSet):

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-10 20:10
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('ipam', '0020_ipaddress_add_role_carp'),
('dcim', '0049_rackreservation_change_user'),
]
operations = [
migrations.AddField(
model_name='interface',
name='mode',
field=models.PositiveSmallIntegerField(blank=True, choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], null=True),
),
migrations.AddField(
model_name='interface',
name='tagged_vlans',
field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'),
),
migrations.AddField(
model_name='interface',
name='untagged_vlan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
),
]

View File

@@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-15 18:56
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0003_unicode_literals'),
('dcim', '0050_interface_vlan_tagging'),
]
operations = [
migrations.AddField(
model_name='rackreservation',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'),
),
]

View File

@@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-27 17:27
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0051_rackreservation_tenant'),
]
operations = [
migrations.CreateModel(
name='VirtualChassis',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('domain', models.CharField(blank=True, max_length=30)),
('master', models.OneToOneField(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')),
],
),
migrations.AddField(
model_name='device',
name='virtual_chassis',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'),
),
migrations.AddField(
model_name='device',
name='vc_position',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
),
migrations.AddField(
model_name='device',
name='vc_priority',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]),
),
migrations.AlterUniqueTogether(
name='device',
unique_together=set([('virtual_chassis', 'vc_position'), ('rack', 'position', 'face')]),
),
]

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-12-19 20:56
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0052_virtual_chassis'),
]
operations = [
migrations.AddField(
model_name='platform',
name='manufacturer',
field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='platforms', to='dcim.Manufacturer'),
),
migrations.AlterField(
model_name='platform',
name='napalm_driver',
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'),
),
]

View File

@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-01-25 18:21
from __future__ import unicode_literals
from django.db import migrations, models
import timezone_field.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0053_platform_manufacturer'),
]
operations = [
migrations.AddField(
model_name='site',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='site',
name='status',
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1),
),
migrations.AddField(
model_name='site',
name='time_zone',
field=timezone_field.fields.TimeZoneField(blank=True),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,28 +0,0 @@
from __future__ import unicode_literals
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from .models import Device, VirtualChassis
@receiver(post_save, sender=VirtualChassis)
def assign_virtualchassis_master(instance, created, **kwargs):
"""
When a VirtualChassis is created, automatically assign its master device to the VC.
"""
# Default to 1 but don't overwrite an existing position (see #2087)
if instance.master.vc_position is not None:
vc_position = instance.master.vc_position
else:
vc_position = 1
if created:
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=vc_position)
@receiver(pre_delete, sender=VirtualChassis)
def clear_virtualchassis_members(instance, **kwargs):
"""
When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.
"""
Device.objects.filter(virtual_chassis=instance.pk).update(vc_position=None, vc_priority=None)

View File

@@ -3,15 +3,14 @@ from __future__ import unicode_literals
import django_tables2 as tables
from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
from utilities.tables import BaseTable, ToggleColumn
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, InventoryItem,
Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, Region, Site, VirtualChassis,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
)
REGION_LINK = """
{% if record.get_children %}
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
@@ -41,32 +40,18 @@ DEVICE_LINK = """
"""
REGION_ACTIONS = """
<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_region %}
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
RACKGROUP_ACTIONS = """
<a href="{% url 'dcim:rackgroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
<i class="fa fa-eye"></i>
</a>
{% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning" title="Edit">
<i class="glyphicon glyphicon-pencil"></i>
</a>
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
RACKROLE_ACTIONS = """
<a href="{% url 'dcim:rackrole_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_rackrole %}
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
@@ -80,57 +65,25 @@ RACK_ROLE = """
{% endif %}
"""
RACK_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
"""
RACKRESERVATION_ACTIONS = """
<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_rackreservation %}
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
MANUFACTURER_ACTIONS = """
<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_manufacturer %}
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
DEVICEROLE_ACTIONS = """
<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_devicerole %}
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
DEVICEROLE_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value }}</a>
"""
DEVICEROLE_VM_COUNT = """
<a href="{% url 'virtualization:virtualmachine_list' %}?role={{ record.slug }}">{{ value }}</a>
"""
PLATFORM_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?platform={{ record.slug }}">{{ value }}</a>
"""
PLATFORM_VM_COUNT = """
<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value }}</a>
MANUFACTURER_ACTIONS = """
{% if perms.dcim.change_manufacturer %}
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
PLATFORM_ACTIONS = """
<a href="{% url 'dcim:platform_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_platform %}
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
@@ -140,7 +93,7 @@ DEVICE_ROLE = """
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
"""
STATUS_LABEL = """
DEVICE_STATUS = """
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
"""
@@ -154,24 +107,11 @@ SUBDEVICE_ROLE_TEMPLATE = """
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}&mdash;{% endif %}
"""
DEVICETYPE_INSTANCES_TEMPLATE = """
<a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
"""
UTILIZATION_GRAPH = """
{% load helpers %}
{% utilization_graph value %}
"""
VIRTUALCHASSIS_ACTIONS = """
<a href="{% url 'dcim:virtualchassis_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_virtualchassis %}
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
#
# Regions
@@ -199,14 +139,27 @@ class RegionTable(BaseTable):
class SiteTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
name = tables.LinkColumn()
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
tenant = tables.TemplateColumn(template_code=COL_TENANT)
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(BaseTable.Meta):
model = Site
fields = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
fields = ('pk', 'name', 'facility', 'region', 'tenant', 'asn')
class SiteDetailTable(SiteTable):
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')
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')
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
class Meta(SiteTable.Meta):
fields = (
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count',
)
#
@@ -215,21 +168,12 @@ class SiteTable(BaseTable):
class RackGroupTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
site = tables.LinkColumn(
viewname='dcim:site',
args=[Accessor('site.slug')],
verbose_name='Site'
)
rack_count = tables.Column(
verbose_name='Racks'
)
slug = tables.Column()
actions = tables.TemplateColumn(
template_code=RACKGROUP_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
name = tables.LinkColumn(verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
rack_count = tables.Column(verbose_name='Racks')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = RackGroup
@@ -260,10 +204,10 @@ class RackRoleTable(BaseTable):
class RackTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
name = tables.LinkColumn()
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
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')
@@ -273,16 +217,12 @@ class RackTable(BaseTable):
class RackDetailTable(RackTable):
device_count = tables.TemplateColumn(
template_code=RACK_DEVICE_COUNT,
verbose_name='Devices'
)
devices = tables.Column(accessor=Accessor('device_count'))
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(RackTable.Meta):
fields = (
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
'get_utilization',
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
)
@@ -291,7 +231,7 @@ class RackImportTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
facility_id = tables.Column(verbose_name='Facility ID')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
u_height = tables.Column(verbose_name='Height (U)')
class Meta(BaseTable.Meta):
@@ -305,7 +245,6 @@ class RackImportTable(BaseTable):
class RackReservationTable(BaseTable):
pk = ToggleColumn()
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
unit_list = tables.Column(orderable=False, verbose_name='Units')
actions = tables.TemplateColumn(
@@ -314,7 +253,7 @@ class RackReservationTable(BaseTable):
class Meta(BaseTable.Meta):
model = RackReservation
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'description', 'actions')
#
@@ -325,14 +264,13 @@ class ManufacturerTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
devicetype_count = tables.Column(verbose_name='Device Types')
platform_count = tables.Column(verbose_name='Platforms')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = Manufacturer
fields = ('pk', 'name', 'devicetype_count', 'platform_count', 'slug', 'actions')
fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions')
#
@@ -341,23 +279,13 @@ class ManufacturerTable(BaseTable):
class DeviceTypeTable(BaseTable):
pk = ToggleColumn()
model = tables.LinkColumn(
viewname='dcim:devicetype',
args=[Accessor('pk')],
verbose_name='Device Type'
)
is_full_depth = BooleanColumn(verbose_name='Full Depth')
is_console_server = BooleanColumn(verbose_name='CS')
is_pdu = BooleanColumn(verbose_name='PDU')
is_network_device = BooleanColumn(verbose_name='Net')
subdevice_role = tables.TemplateColumn(
template_code=SUBDEVICE_ROLE_TEMPLATE,
verbose_name='Subdevice Role'
)
instance_count = tables.TemplateColumn(
template_code=DEVICETYPE_INSTANCES_TEMPLATE,
verbose_name='Instances'
)
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
is_console_server = tables.BooleanColumn(verbose_name='CS')
is_pdu = tables.BooleanColumn(verbose_name='PDU')
is_network_device = tables.BooleanColumn(verbose_name='Net')
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
instance_count = tables.Column(verbose_name='Instances')
class Meta(BaseTable.Meta):
model = DeviceType
@@ -433,25 +361,12 @@ class DeviceBayTemplateTable(BaseTable):
class DeviceRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.TemplateColumn(
template_code=DEVICEROLE_DEVICE_COUNT,
accessor=Accessor('devices.count'),
orderable=False,
verbose_name='Devices'
)
vm_count = tables.TemplateColumn(
template_code=DEVICEROLE_VM_COUNT,
accessor=Accessor('virtual_machines.count'),
orderable=False,
verbose_name='VMs'
)
device_count = tables.Column(verbose_name='Devices')
vm_count = tables.Column(verbose_name='VMs')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(
template_code=DEVICEROLE_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = DeviceRole
@@ -464,27 +379,15 @@ class DeviceRoleTable(BaseTable):
class PlatformTable(BaseTable):
pk = ToggleColumn()
device_count = tables.TemplateColumn(
template_code=PLATFORM_DEVICE_COUNT,
accessor=Accessor('devices.count'),
orderable=False,
verbose_name='Devices'
)
vm_count = tables.TemplateColumn(
template_code=PLATFORM_VM_COUNT,
accessor=Accessor('virtual_machines.count'),
orderable=False,
verbose_name='VMs'
)
actions = tables.TemplateColumn(
template_code=PLATFORM_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = Platform
fields = ('pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'actions')
fields = ('pk', 'name', 'device_count', 'slug', 'napalm_driver', 'actions')
#
@@ -493,12 +396,9 @@ class PlatformTable(BaseTable):
class DeviceTable(BaseTable):
pk = ToggleColumn()
name = tables.TemplateColumn(
order_by=('_nat1', '_nat2', '_nat3'),
template_code=DEVICE_LINK
)
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
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')
@@ -524,8 +424,8 @@ class DeviceDetailTable(DeviceTable):
class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
position = tables.Column(verbose_name='Position')
@@ -621,38 +521,5 @@ class InterfaceConnectionTable(BaseTable):
interface_b = tables.Column(verbose_name='Interface B')
class Meta(BaseTable.Meta):
model = InterfaceConnection
model = Interface
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
#
# InventoryItems
#
class InventoryItemTable(BaseTable):
pk = ToggleColumn()
device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')])
manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer')
class Meta(BaseTable.Meta):
model = InventoryItem
fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description')
#
# Virtual chassis
#
class VirtualChassisTable(BaseTable):
pk = ToggleColumn()
master = tables.LinkColumn()
member_count = tables.Column(verbose_name='Members')
actions = tables.TemplateColumn(
template_code=VIRTUALCHASSIS_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = VirtualChassis
fields = ('pk', 'master', 'domain', 'member_count', 'actions')

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ class DeviceTestCase(TestCase):
'face': RACK_FACE_FRONT,
'position': 41,
'platform': get_id(Platform, 'juniper-junos'),
'status': DEVICE_STATUS_ACTIVE,
'status': STATUS_ACTIVE,
})
self.assertTrue(test.is_valid(), test.fields['position'].choices)
self.assertTrue(test.save())
@@ -43,7 +43,7 @@ class DeviceTestCase(TestCase):
'face': RACK_FACE_FRONT,
'position': 1,
'platform': get_id(Platform, 'juniper-junos'),
'status': DEVICE_STATUS_ACTIVE,
'status': STATUS_ACTIVE,
})
self.assertFalse(test.is_valid())
@@ -59,7 +59,7 @@ class DeviceTestCase(TestCase):
'face': None,
'position': None,
'platform': None,
'status': DEVICE_STATUS_ACTIVE,
'status': STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())
@@ -76,7 +76,7 @@ class DeviceTestCase(TestCase):
'face': RACK_FACE_REAR,
'position': None,
'platform': None,
'status': DEVICE_STATUS_ACTIVE,
'status': STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())

View File

@@ -2,14 +2,12 @@ from __future__ import unicode_literals
from django.conf.urls import url
from extras.views import ObjectChangeLogView, ImageAttachmentEditView
from extras.views import ImageAttachmentEditView
from ipam.views import ServiceCreateView
from secrets.views import secret_add
from .models import Device, Rack, Site
from . import views
from .models import (
Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VirtualChassis,
)
app_name = 'dcim'
urlpatterns = [
@@ -20,7 +18,6 @@ urlpatterns = [
url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'),
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
url(r'^regions/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
# Sites
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
@@ -30,7 +27,6 @@ urlpatterns = [
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
url(r'^sites/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
# Rack groups
@@ -39,7 +35,6 @@ urlpatterns = [
url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
url(r'^rack-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
# Rack roles
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
@@ -47,7 +42,6 @@ urlpatterns = [
url(r'^rack-roles/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
url(r'^rack-roles/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
# Rack reservations
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
@@ -55,7 +49,6 @@ urlpatterns = [
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
url(r'^rack-reservations/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
# Racks
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
@@ -67,7 +60,6 @@ urlpatterns = [
url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
url(r'^racks/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
@@ -77,7 +69,6 @@ urlpatterns = [
url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
url(r'^manufacturers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
# Device types
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
@@ -88,7 +79,6 @@ urlpatterns = [
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
url(r'^device-types/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
# Console port templates
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
@@ -121,7 +111,6 @@ urlpatterns = [
url(r'^device-roles/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
url(r'^device-roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
# Platforms
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
@@ -129,7 +118,6 @@ urlpatterns = [
url(r'^platforms/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'),
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
url(r'^platforms/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
# Devices
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
@@ -141,8 +129,6 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
url(r'^devices/(?P<pk>\d+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
url(r'^devices/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
@@ -155,8 +141,8 @@ urlpatterns = [
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.ConsolePortConnectView.as_view(), name='consoleport_connect'),
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.ConsolePortDisconnectView.as_view(), name='consoleport_disconnect'),
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
@@ -165,18 +151,17 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.ConsoleServerPortConnectView.as_view(), name='consoleserverport_connect'),
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.ConsoleServerPortDisconnectView.as_view(), name='consoleserverport_disconnect'),
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
url(r'^console-server-ports/rename/$', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
# Power ports
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.PowerPortConnectView.as_view(), name='powerport_connect'),
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.PowerPortDisconnectView.as_view(), name='powerport_disconnect'),
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
@@ -185,11 +170,10 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
url(r'^devices/(?P<pk>\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.PowerOutletConnectView.as_view(), name='poweroutlet_connect'),
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.PowerOutletDisconnectView.as_view(), name='poweroutlet_disconnect'),
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
url(r'^power-outlets/rename/$', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
# Interfaces
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
@@ -197,13 +181,10 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'),
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'),
url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'),
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
# Device bays
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
@@ -211,18 +192,13 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
url(r'^device-bays/rename/$', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
# Inventory items
url(r'^inventory-items/$', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
url(r'^inventory-items/import/$', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
url(r'^inventory-items/edit/$', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
url(r'^inventory-items/delete/$', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
# Console/power/interface connections
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
@@ -232,13 +208,4 @@ urlpatterns = [
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
# Virtual chassis
url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
url(r'^virtual-chassis/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
]

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -4,12 +4,7 @@ from django import forms
from django.contrib import admin
from django.utils.safestring import mark_safe
from utilities.forms import LaxURLField
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
from .models import (
ConfigContext, CustomField, CustomFieldChoice, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction,
Webhook,
)
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction
def order_content_types(field):
@@ -20,37 +15,6 @@ def order_content_types(field):
field.choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
#
# Webhooks
#
class WebhookForm(forms.ModelForm):
payload_url = LaxURLField(
label='URL'
)
class Meta:
model = Webhook
exclude = []
def __init__(self, *args, **kwargs):
super(WebhookForm, self).__init__(*args, **kwargs)
order_content_types(self.fields['obj_type'])
@admin.register(Webhook)
class WebhookAdmin(admin.ModelAdmin):
list_display = [
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update',
'type_delete', 'ssl_verification',
]
form = WebhookForm
def models(self, obj):
return ', '.join([ct.name for ct in obj.obj_type.all()])
#
# Custom fields
#
@@ -75,7 +39,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
@admin.register(CustomField)
class CustomFieldAdmin(admin.ModelAdmin):
inlines = [CustomFieldChoiceAdmin]
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
list_display = ['name', 'models', 'type', 'required', 'default', 'weight', 'description']
form = CustomFieldForm
def models(self, obj):
@@ -127,58 +91,6 @@ class TopologyMapAdmin(admin.ModelAdmin):
}
#
# Config contexts
#
@admin.register(ConfigContext)
class ConfigContextAdmin(admin.ModelAdmin):
list_display = ['name', 'weight']
#
# Change logging
#
@admin.register(ObjectChange)
class ObjectChangeAdmin(admin.ModelAdmin):
actions = None
fields = ['time', 'changed_object_type', 'display_object', 'action', 'display_user', 'request_id', 'object_data']
list_display = ['time', 'changed_object_type', 'display_object', 'display_action', 'display_user', 'request_id']
list_filter = ['time', 'action', 'user__username']
list_select_related = ['changed_object_type', 'user']
readonly_fields = fields
search_fields = ['user_name', 'object_repr', 'request_id']
def has_add_permission(self, request):
return False
def display_user(self, obj):
if obj.user is not None:
return obj.user
else:
return '{} (deleted)'.format(obj.user_name)
display_user.short_description = 'user'
def display_action(self, obj):
icon = {
OBJECTCHANGE_ACTION_CREATE: 'addlink',
OBJECTCHANGE_ACTION_UPDATE: 'changelink',
OBJECTCHANGE_ACTION_DELETE: 'deletelink',
}
return mark_safe('<span class="{}">{}</span>'.format(icon[obj.action], obj.get_action_display()))
display_action.short_description = 'action'
def display_object(self, obj):
if hasattr(obj.changed_object, 'get_absolute_url'):
return mark_safe('<a href="{}">{}</a>'.format(obj.changed_object.get_absolute_url(), obj.changed_object))
elif obj.changed_object is not None:
return obj.changed_object
else:
return '{} (deleted)'.format(obj.object_repr)
display_object.short_description = 'object'
#
# User actions
#

View File

@@ -1,14 +1,15 @@
from __future__ import unicode_literals
from datetime import datetime
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from extras.models import (
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
)
from utilities.api import ValidatedModelSerializer
@@ -38,15 +39,6 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
# Data validation
if value not in [None, '']:
# Validate integer
if cf.type == CF_TYPE_INTEGER:
try:
int(value)
except ValueError:
raise ValidationError(
"Invalid value for integer field {}: {}".format(field_name, value)
)
# Validate boolean
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
raise ValidationError(

View File

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

View File

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

View File

@@ -1,22 +1,18 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from taggit.models import Tag
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
from django.contrib.contenttypes.models import ContentType
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from extras import filters
from extras.models import (
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
UserAction,
)
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
from extras.reports import get_report, get_reports
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, WritableSerializerMixin
from . import serializers
@@ -69,9 +65,10 @@ class CustomFieldModelViewSet(ModelViewSet):
# Graphs
#
class GraphViewSet(ModelViewSet):
class GraphViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Graph.objects.all()
serializer_class = serializers.GraphSerializer
write_serializer_class = serializers.WritableGraphSerializer
filter_class = filters.GraphFilter
@@ -79,7 +76,7 @@ class GraphViewSet(ModelViewSet):
# Export templates
#
class ExportTemplateViewSet(ModelViewSet):
class ExportTemplateViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ExportTemplate.objects.all()
serializer_class = serializers.ExportTemplateSerializer
filter_class = filters.ExportTemplateFilter
@@ -89,12 +86,13 @@ class ExportTemplateViewSet(ModelViewSet):
# Topology maps
#
class TopologyMapViewSet(ModelViewSet):
class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet):
queryset = TopologyMap.objects.select_related('site')
serializer_class = serializers.TopologyMapSerializer
write_serializer_class = serializers.WritableTopologyMapSerializer
filter_class = filters.TopologyMapFilter
@action(detail=True)
@detail_route()
def render(self, request, pk):
tmap = get_object_or_404(TopologyMap, pk=pk)
@@ -102,7 +100,7 @@ class TopologyMapViewSet(ModelViewSet):
try:
data = tmap.render(img_format=img_format)
except Exception:
except:
return HttpResponse(
"There was an error generating the requested graph. Ensure that the GraphViz executables have been "
"installed correctly."
@@ -114,32 +112,14 @@ class TopologyMapViewSet(ModelViewSet):
return response
#
# Tags
#
class TagViewSet(ModelViewSet):
queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items'))
serializer_class = serializers.TagSerializer
filter_class = filters.TagFilter
#
# Image attachments
#
class ImageAttachmentViewSet(ModelViewSet):
class ImageAttachmentViewSet(WritableSerializerMixin, ModelViewSet):
queryset = ImageAttachment.objects.all()
serializer_class = serializers.ImageAttachmentSerializer
#
# Config contexts
#
class ConfigContextViewSet(ModelViewSet):
queryset = ConfigContext.objects.prefetch_related('regions', 'sites', 'roles', 'platforms', 'tenants')
serializer_class = serializers.ConfigContextSerializer
write_serializer_class = serializers.WritableImageAttachmentSerializer
#
@@ -199,7 +179,7 @@ class ReportViewSet(ViewSet):
return Response(serializer.data)
@action(detail=True, methods=['post'])
@detail_route(methods=['post'])
def run(self, request, pk):
"""
Run a Report and create a new ReportResult, overwriting any previous result for the Report.
@@ -218,19 +198,6 @@ class ReportViewSet(ViewSet):
return Response(serializer.data)
#
# Change logging
#
class ObjectChangeViewSet(ReadOnlyModelViewSet):
"""
Retrieve a list of recent changes.
"""
queryset = ObjectChange.objects.select_related('user')
serializer_class = serializers.ObjectChangeSerializer
filter_class = filters.ObjectChangeFilter
#
# User activity
#

View File

@@ -1,33 +0,0 @@
from __future__ import unicode_literals
from django.apps import AppConfig
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
class ExtrasConfig(AppConfig):
name = "extras"
def ready(self):
# Check that we can connect to the configured Redis database if webhooks are enabled.
if settings.WEBHOOKS_ENABLED:
try:
import redis
except ImportError:
raise ImproperlyConfigured(
"WEBHOOKS_ENABLED is True but the redis Python package is not installed. (Try 'pip install "
"redis'.)"
)
try:
rs = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DATABASE,
password=settings.REDIS_PASSWORD or None,
)
rs.ping()
except redis.exceptions.ConnectionError:
raise ImproperlyConfigured(
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
"configuration.py."
)

View File

@@ -3,12 +3,11 @@ from __future__ import unicode_literals
# Models which support custom fields
CUSTOMFIELD_MODELS = (
'provider', 'circuit', # Circuits
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
'secret', # Secrets
'tenant', # Tenancy
'cluster', 'virtualmachine', # Virtualization
'provider', 'circuit', # Circuits
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'tenant', # Tenancy
'cluster', 'virtualmachine', # Virtualization
)
# Custom field types
@@ -27,16 +26,6 @@ CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_SELECT, 'Selection'),
)
# Custom field filter logic choices
CF_FILTER_DISABLED = 0
CF_FILTER_LOOSE = 1
CF_FILTER_EXACT = 2
CF_FILTER_CHOICES = (
(CF_FILTER_DISABLED, 'Disabled'),
(CF_FILTER_LOOSE, 'Loose'),
(CF_FILTER_EXACT, 'Exact'),
)
# Graph types
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
@@ -51,33 +40,12 @@ GRAPH_TYPE_CHOICES = (
EXPORTTEMPLATE_MODELS = [
'provider', 'circuit', # Circuits
'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM
'consoleport', 'powerport', 'interfaceconnection', 'virtualchassis', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
'secret', # Secrets
'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'tenant', # Tenancy
'cluster', 'virtualmachine', # Virtualization
]
# Topology map types
TOPOLOGYMAP_TYPE_NETWORK = 1
TOPOLOGYMAP_TYPE_CONSOLE = 2
TOPOLOGYMAP_TYPE_POWER = 3
TOPOLOGYMAP_TYPE_CHOICES = (
(TOPOLOGYMAP_TYPE_NETWORK, 'Network'),
(TOPOLOGYMAP_TYPE_CONSOLE, 'Console'),
(TOPOLOGYMAP_TYPE_POWER, 'Power'),
)
# Change log actions
OBJECTCHANGE_ACTION_CREATE = 1
OBJECTCHANGE_ACTION_UPDATE = 2
OBJECTCHANGE_ACTION_DELETE = 3
OBJECTCHANGE_ACTION_CHOICES = (
(OBJECTCHANGE_ACTION_CREATE, 'Created'),
(OBJECTCHANGE_ACTION_UPDATE, 'Updated'),
(OBJECTCHANGE_ACTION_DELETE, 'Deleted'),
)
# User action types
ACTION_CREATE = 1
ACTION_IMPORT = 2
@@ -109,23 +77,3 @@ LOG_LEVEL_CODES = {
LOG_WARNING: 'warning',
LOG_FAILURE: 'failure',
}
# webhook content types
WEBHOOK_CT_JSON = 1
WEBHOOK_CT_X_WWW_FORM_ENCODED = 2
WEBHOOK_CT_CHOICES = (
(WEBHOOK_CT_JSON, 'application/json'),
(WEBHOOK_CT_X_WWW_FORM_ENCODED, 'application/x-www-form-urlencoded'),
)
# Models which support registered webhooks
WEBHOOK_MODELS = (
'provider', 'circuit', # Circuits
'site', 'rack', 'devicetype', 'device', 'virtualchassis', # DCIM
'consoleport', 'consoleserverport', 'powerport', 'poweroutlet',
'interface', 'devicebay', 'inventoryitem',
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
'secret', # Secrets
'tenant', # Tenancy
'cluster', 'virtualmachine', # Virtualization
)

View File

@@ -1,14 +1,12 @@
from __future__ import unicode_literals
import django_filters
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from taggit.models import Tag
from dcim.models import Site
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
from .models import CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
from .models import CF_TYPE_SELECT, CustomField, Graph, ExportTemplate, TopologyMap, UserAction
class CustomFieldFilter(django_filters.Filter):
@@ -16,9 +14,8 @@ class CustomFieldFilter(django_filters.Filter):
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
"""
def __init__(self, custom_field, *args, **kwargs):
self.cf_type = custom_field.type
self.filter_logic = custom_field.filter_logic
def __init__(self, cf_type, *args, **kwargs):
self.cf_type = cf_type
super(CustomFieldFilter, self).__init__(*args, **kwargs)
def filter(self, queryset, value):
@@ -44,19 +41,10 @@ class CustomFieldFilter(django_filters.Filter):
except ValueError:
return queryset.none()
# Apply the assigned filter logic (exact or loose)
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
queryset = queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__serialized_value=value
)
else:
queryset = queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__serialized_value__icontains=value
)
return queryset
return queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__serialized_value__icontains=value,
)
class CustomFieldFilterSet(django_filters.FilterSet):
@@ -68,9 +56,9 @@ class CustomFieldFilterSet(django_filters.FilterSet):
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
obj_type = ContentType.objects.get_for_model(self._meta.model)
custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
for cf in custom_fields:
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, custom_field=cf)
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type)
class GraphFilter(django_filters.FilterSet):
@@ -87,25 +75,6 @@ class ExportTemplateFilter(django_filters.FilterSet):
fields = ['content_type', 'name']
class TagFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class Meta:
model = Tag
fields = ['name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(slug__icontains=value)
)
class TopologyMapFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
@@ -124,26 +93,6 @@ class TopologyMapFilter(django_filters.FilterSet):
fields = ['name', 'slug']
class ObjectChangeFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
time = django_filters.DateTimeFromToRangeFilter()
class Meta:
model = ObjectChange
fields = ['user', 'user_name', 'request_id', 'action', 'changed_object_type', 'object_repr']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(user_name__icontains=value) |
Q(object_repr__icontains=value)
)
class UserActionFilter(django_filters.FilterSet):
username = django_filters.ModelMultipleChoiceFilter(
name='user__username',

View File

@@ -1,44 +1,32 @@
from __future__ import unicode_literals
from collections import OrderedDict
from django import forms
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from mptt.forms import TreeNodeMultipleChoiceField
from taggit.forms import TagField
from taggit.models import Tag
from dcim.models import Region
from utilities.forms import add_blank_choice, BootstrapMixin, BulkEditForm, LaxURLField, JSONField, SlugField
from .constants import (
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
OBJECTCHANGE_ACTION_CHOICES,
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
from .models import (
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue,
ImageAttachment,
)
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange
#
# Custom fields
#
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
"""
Retrieve all CustomFields applicable to the given ContentType
"""
field_dict = OrderedDict()
custom_fields = CustomField.objects.filter(obj_type=content_type)
kwargs = {'obj_type': content_type}
if filterable_only:
custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
kwargs['is_filterable'] = True
custom_fields = CustomField.objects.filter(**kwargs)
for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name))
initial = cf.default if not bulk_edit else None
# Integer
if cf.type == CF_TYPE_INTEGER:
field = forms.IntegerField(required=cf.required, initial=initial)
field = forms.IntegerField(required=cf.required, initial=cf.default)
# Boolean
elif cf.type == CF_TYPE_BOOLEAN:
@@ -47,41 +35,33 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
(1, 'True'),
(0, 'False'),
)
if initial is not None and initial.lower() in ['true', 'yes', '1']:
if cf.default.lower() in ['true', 'yes', '1']:
initial = 1
elif initial is not None and initial.lower() in ['false', 'no', '0']:
elif cf.default.lower() in ['false', 'no', '0']:
initial = 0
else:
initial = None
field = forms.NullBooleanField(
required=cf.required, initial=initial, widget=forms.Select(choices=choices)
)
field = forms.NullBooleanField(required=cf.required, initial=initial,
widget=forms.Select(choices=choices))
# Date
elif cf.type == CF_TYPE_DATE:
field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
field = forms.DateField(required=cf.required, initial=cf.default, help_text="Date format: YYYY-MM-DD")
# Select
elif cf.type == CF_TYPE_SELECT:
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required or bulk_edit or filterable_only:
choices = [(None, '---------')] + choices
# Check for a default choice
default_choice = None
if initial:
try:
default_choice = cf.choices.get(value=initial).pk
except ObjectDoesNotExist:
pass
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
# URL
elif cf.type == CF_TYPE_URL:
field = LaxURLField(required=cf.required, initial=initial)
field = LaxURLField(required=cf.required, initial=cf.default)
# Text
else:
field = forms.CharField(max_length=255, required=cf.required, initial=initial)
field = forms.CharField(max_length=255, required=cf.required, initial=cf.default)
field.model = cf
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()
@@ -182,87 +162,8 @@ class CustomFieldFilterForm(forms.Form):
self.fields[name] = field
#
# Tags
#
class TagForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
class Meta:
model = Tag
fields = ['name', 'slug']
class AddRemoveTagsForm(forms.Form):
def __init__(self, *args, **kwargs):
super(AddRemoveTagsForm, self).__init__(*args, **kwargs)
# Add add/remove tags fields
self.fields['add_tags'] = TagField(required=False)
self.fields['remove_tags'] = TagField(required=False)
#
# Config contexts
#
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
regions = TreeNodeMultipleChoiceField(
queryset=Region.objects.all(),
required=False
)
data = JSONField()
class Meta:
model = ConfigContext
fields = [
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenants', 'data',
]
#
# Image attachments
#
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ImageAttachment
fields = ['name', 'image']
#
# Change logging
#
class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = ObjectChange
q = forms.CharField(
required=False,
label='Search'
)
# TODO: Change time_0 and time_1 to time_after and time_before for django-filter==2.0
time_0 = forms.DateTimeField(
label='After',
required=False,
widget=forms.TextInput(
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
)
)
time_1 = forms.DateTimeField(
label='Before',
required=False,
widget=forms.TextInput(
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
)
)
action = forms.ChoiceField(
choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
required=False
)
user = forms.ModelChoiceField(
queryset=User.objects.order_by('username'),
required=False
)

View File

@@ -10,6 +10,7 @@ from django.conf import settings
from django.core.management.base import BaseCommand
from django.db.models import Model
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
BANNER_TEXT = """### NetBox interactive shell ({node})

View File

@@ -1,14 +1,14 @@
from __future__ import unicode_literals
from getpass import getpass
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException
from dcim.models import DEVICE_STATUS_ACTIVE, Device, InventoryItem, Site
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
class Command(BaseCommand):
@@ -41,7 +41,7 @@ class Command(BaseCommand):
self.password = getpass("Password: ")
# Attempt to inventory only active devices
device_list = Device.objects.filter(status=DEVICE_STATUS_ACTIVE)
device_list = Device.objects.filter(status=STATUS_ACTIVE)
# --site: Include only devices belonging to specified site(s)
if options['site']:

View File

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.core.management.base import BaseCommand
from django.utils import timezone
from extras.models import ReportResult
from extras.reports import get_reports

View File

@@ -1,71 +0,0 @@
from __future__ import unicode_literals
from datetime import timedelta
import random
import threading
import uuid
from django.conf import settings
from django.db.models.signals import post_delete, post_save
from django.utils import timezone
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
from .models import ObjectChange
_thread_locals = threading.local()
def mark_object_changed(instance, **kwargs):
"""
Mark an object as having been created, saved, or updated. At the end of the request, this change will be recorded.
We have to wait until the *end* of the request to the serialize the object, because related fields like tags and
custom fields have not yet been updated when the post_save signal is emitted.
"""
if not hasattr(instance, 'log_change'):
return
# Determine what action is being performed. The post_save signal sends a `created` boolean, whereas post_delete
# does not.
if 'created' in kwargs:
action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
else:
action = OBJECTCHANGE_ACTION_DELETE
_thread_locals.changed_objects.append((instance, action))
class ChangeLoggingMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Initialize the list of changed objects
_thread_locals.changed_objects = []
# Assign a random unique ID to the request. This will be used to associate multiple object changes made during
# the same request.
request.id = uuid.uuid4()
# Connect mark_object_changed to the post_save and post_delete receivers
post_save.connect(mark_object_changed, dispatch_uid='record_object_saved')
post_delete.connect(mark_object_changed, dispatch_uid='record_object_deleted')
# Process the request
response = self.get_response(request)
# Record object changes
for obj, action in _thread_locals.changed_objects:
if obj.pk:
obj.log_change(request.user, request.id, action)
# Housekeeping: 1% chance of clearing out expired ObjectChanges
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
purged_count, _ = ObjectChange.objects.filter(
time__lt=cutoff
).delete()
return response

View File

@@ -4,6 +4,14 @@ from __future__ import unicode_literals
from django.db import migrations, models
from extras.models import TopologyMap
def commas_to_semicolons(apps, schema_editor):
for tm in TopologyMap.objects.filter(device_patterns__contains=','):
tm.device_patterns = tm.device_patterns.replace(',', ';')
tm.save()
class Migration(migrations.Migration):
@@ -17,4 +25,5 @@ class Migration(migrations.Migration):
name='device_patterns',
field=models.TextField(help_text=b'Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'),
),
migrations.RunPython(commas_to_semicolons),
]

View File

@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='customfield',
name='default',
field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans.', max_length=100),
field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100),
),
migrations.AlterField(
model_name='customfield',

View File

@@ -2,7 +2,6 @@
# 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
@@ -19,7 +18,7 @@ def verify_postgresql_version(apps, schema_editor):
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
row = cursor.fetchone()
pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
pg_version = row[0].split()[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))

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-02-15 16:28
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0008_reports'),
]
operations = [
migrations.AddField(
model_name='topologymap',
name='type',
field=models.PositiveSmallIntegerField(choices=[(1, 'Network'), (2, 'Console'), (3, 'Power')], default=1),
),
]

View File

@@ -1,51 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-02-21 19:48
from __future__ import unicode_literals
from django.db import migrations, models
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
def is_filterable_to_filter_logic(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField')
CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED)
CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE)
# Select fields match on primary key only
CustomField.objects.filter(is_filterable=True, type=CF_TYPE_SELECT).update(filter_logic=CF_FILTER_EXACT)
def filter_logic_to_is_filterable(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField')
CustomField.objects.filter(filter_logic=CF_FILTER_DISABLED).update(is_filterable=False)
CustomField.objects.exclude(filter_logic=CF_FILTER_DISABLED).update(is_filterable=True)
class Migration(migrations.Migration):
dependencies = [
('extras', '0009_topologymap_type'),
]
operations = [
migrations.AddField(
model_name='customfield',
name='filter_logic',
field=models.PositiveSmallIntegerField(choices=[(0, 'Disabled'), (1, 'Loose'), (2, 'Exact')], default=1, help_text='Loose matches any instance of a given string; exact matches the entire field.'),
),
migrations.AlterField(
model_name='customfield',
name='required',
field=models.BooleanField(default=False, help_text='If true, this field is required when creating new objects or editing an existing object.'),
),
migrations.AlterField(
model_name='customfield',
name='weight',
field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form.'),
),
migrations.RunPython(is_filterable_to_filter_logic, filter_logic_to_is_filterable),
migrations.RemoveField(
model_name='customfield',
name='is_filterable',
),
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 2.0.3 on 2018-03-30 14:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('extras', '0010_customfield_filter_logic'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='obj_type',
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
),
migrations.AlterField(
model_name='customfieldchoice',
name='field',
field=models.ForeignKey(limit_choices_to={'type': 600}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'),
),
migrations.AlterField(
model_name='exporttemplate',
name='content_type',
field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interfaceconnection', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
),
]

View File

@@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-05-30 17:55
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('extras', '0011_django2'),
]
operations = [
migrations.CreateModel(
name='Webhook',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=150, unique=True)),
('type_create', models.BooleanField(default=False, help_text='Call this webhook when a matching object is created.')),
('type_update', models.BooleanField(default=False, help_text='Call this webhook when a matching object is updated.')),
('type_delete', models.BooleanField(default=False, help_text='Call this webhook when a matching object is deleted.')),
('payload_url', models.CharField(help_text='A POST will be sent to this URL when the webhook is called.', max_length=500, verbose_name='URL')),
('http_content_type', models.PositiveSmallIntegerField(choices=[(1, 'application/json'), (2, 'application/x-www-form-urlencoded')], default=1, verbose_name='HTTP content type')),
('secret', models.CharField(blank=True, help_text="When provided, the request will include a 'X-Hook-Signature' header containing a HMAC hex digest of the payload body using the secret as the key. The secret is not transmitted in the request.", max_length=255)),
('enabled', models.BooleanField(default=True)),
('ssl_verification', models.BooleanField(default=True, help_text='Enable SSL certificate verification. Disable with caution!', verbose_name='SSL verification')),
('obj_type', models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types')),
],
),
migrations.AlterUniqueTogether(
name='webhook',
unique_together=set([('payload_url', 'type_create', 'type_update', 'type_delete')]),
),
]

View File

@@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-06-22 18:13
from __future__ import unicode_literals
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
('extras', '0012_webhooks'),
]
operations = [
migrations.CreateModel(
name='ObjectChange',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField(auto_now_add=True)),
('user_name', models.CharField(editable=False, max_length=150)),
('request_id', models.UUIDField(editable=False)),
('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])),
('changed_object_id', models.PositiveIntegerField()),
('related_object_id', models.PositiveIntegerField(blank=True, null=True)),
('object_repr', models.CharField(editable=False, max_length=200)),
('object_data', django.contrib.postgres.fields.jsonb.JSONField(editable=False)),
('changed_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-time'],
},
),
]

View File

@@ -1,45 +0,0 @@
# Generated by Django 2.0.6 on 2018-06-29 13:34
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0005_change_logging'),
('dcim', '0060_change_logging'),
('extras', '0013_objectchange'),
]
operations = [
migrations.CreateModel(
name='ConfigContext',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('weight', models.PositiveSmallIntegerField(default=1000)),
('is_active', models.BooleanField(default=True)),
('description', models.CharField(blank=True, max_length=100)),
('data', django.contrib.postgres.fields.jsonb.JSONField()),
('platforms', models.ManyToManyField(blank=True, related_name='_configcontext_platforms_+', to='dcim.Platform')),
('regions', models.ManyToManyField(blank=True, related_name='_configcontext_regions_+', to='dcim.Region')),
('roles', models.ManyToManyField(blank=True, related_name='_configcontext_roles_+', to='dcim.DeviceRole')),
('sites', models.ManyToManyField(blank=True, related_name='_configcontext_sites_+', to='dcim.Site')),
('tenants', models.ManyToManyField(blank=True, related_name='_configcontext_tenants_+', to='tenancy.Tenant')),
],
options={
'ordering': ['weight', 'name'],
},
),
migrations.AlterField(
model_name='customfield',
name='obj_type',
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
),
migrations.AlterField(
model_name='webhook',
name='obj_type',
field=models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'rackgroup', 'device', 'interface', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vlangroup', 'vrf', 'service', 'tenant', 'tenantgroup', 'cluster', 'clustergroup', 'virtualmachine')}, related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types'),
),
]

View File

@@ -1,9 +1,8 @@
from __future__ import unicode_literals
from collections import OrderedDict
from datetime import date
import graphviz
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@@ -13,92 +12,11 @@ from django.db import models
from django.db.models import Q
from django.http import HttpResponse
from django.template import Template, Context
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe
from dcim.constants import CONNECTION_STATUS_CONNECTED
from utilities.utils import foreground_color
from .constants import *
from .querysets import ConfigContextQuerySet
#
# Webhooks
#
@python_2_unicode_compatible
class Webhook(models.Model):
"""
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
Each Webhook can be limited to firing only on certain actions or certain object types.
"""
obj_type = models.ManyToManyField(
to=ContentType,
related_name='webhooks',
verbose_name='Object types',
limit_choices_to={'model__in': WEBHOOK_MODELS},
help_text="The object(s) to which this Webhook applies."
)
name = models.CharField(
max_length=150,
unique=True
)
type_create = models.BooleanField(
default=False,
help_text="Call this webhook when a matching object is created."
)
type_update = models.BooleanField(
default=False,
help_text="Call this webhook when a matching object is updated."
)
type_delete = models.BooleanField(
default=False,
help_text="Call this webhook when a matching object is deleted."
)
payload_url = models.CharField(
max_length=500,
verbose_name='URL',
help_text="A POST will be sent to this URL when the webhook is called."
)
http_content_type = models.PositiveSmallIntegerField(
choices=WEBHOOK_CT_CHOICES,
default=WEBHOOK_CT_JSON,
verbose_name='HTTP content type'
)
secret = models.CharField(
max_length=255,
blank=True,
help_text="When provided, the request will include a 'X-Hook-Signature' "
"header containing a HMAC hex digest of the payload body using "
"the secret as the key. The secret is not transmitted in "
"the request."
)
enabled = models.BooleanField(
default=True
)
ssl_verification = models.BooleanField(
default=True,
verbose_name='SSL verification',
help_text="Enable SSL certificate verification. Disable with caution!"
)
class Meta:
unique_together = ('payload_url', 'type_create', 'type_update', 'type_delete',)
def __str__(self):
return self.name
def clean(self):
"""
Validate model
"""
if not self.type_create and not self.type_delete and not self.type_update:
raise ValidationError(
"You must select at least one type: create, update, and/or delete."
)
#
@@ -135,51 +53,22 @@ class CustomFieldModel(object):
@python_2_unicode_compatible
class CustomField(models.Model):
obj_type = models.ManyToManyField(
to=ContentType,
related_name='custom_fields',
verbose_name='Object(s)',
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
help_text='The object(s) to which this field applies.'
)
type = models.PositiveSmallIntegerField(
choices=CUSTOMFIELD_TYPE_CHOICES,
default=CF_TYPE_TEXT
)
name = models.CharField(
max_length=50,
unique=True
)
label = models.CharField(
max_length=50,
blank=True,
help_text='Name of the field as displayed to users (if not provided, '
'the field\'s name will be used)'
)
description = models.CharField(
max_length=100,
blank=True
)
required = models.BooleanField(
default=False,
help_text='If true, this field is required when creating new objects '
'or editing an existing object.'
)
filter_logic = models.PositiveSmallIntegerField(
choices=CF_FILTER_CHOICES,
default=CF_FILTER_LOOSE,
help_text='Loose matches any instance of a given string; exact '
'matches the entire field.'
)
default = models.CharField(
max_length=100,
blank=True,
help_text='Default value for the field. Use "true" or "false" for booleans.'
)
weight = models.PositiveSmallIntegerField(
default=100,
help_text='Fields with higher weights appear lower in a form.'
)
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
help_text="The object(s) to which this field applies.")
type = models.PositiveSmallIntegerField(choices=CUSTOMFIELD_TYPE_CHOICES, default=CF_TYPE_TEXT)
name = models.CharField(max_length=50, unique=True)
label = models.CharField(max_length=50, blank=True, help_text="Name of the field as displayed to users (if not "
"provided, the field's name will be used)")
description = models.CharField(max_length=100, blank=True)
required = models.BooleanField(default=False, help_text="Determines whether this field is required when creating "
"new objects or editing an existing object.")
is_filterable = models.BooleanField(default=True, help_text="This field can be used to filter objects.")
default = models.CharField(max_length=100, blank=True, help_text="Default value for the field. Use \"true\" or "
"\"false\" for booleans. N/A for selection "
"fields.")
weight = models.PositiveSmallIntegerField(default=100, help_text="Fields with higher weights appear lower in a "
"form")
class Meta:
ordering = ['weight', 'name']
@@ -210,7 +99,7 @@ class CustomField(models.Model):
"""
Convert a string into the object it represents depending on the type of field
"""
if serialized_value == '':
if serialized_value is '':
return None
if self.type == CF_TYPE_INTEGER:
return int(serialized_value)
@@ -226,24 +115,11 @@ class CustomField(models.Model):
@python_2_unicode_compatible
class CustomFieldValue(models.Model):
field = models.ForeignKey(
to='extras.CustomField',
on_delete=models.CASCADE,
related_name='values'
)
obj_type = models.ForeignKey(
to=ContentType,
on_delete=models.PROTECT,
related_name='+'
)
field = models.ForeignKey('CustomField', related_name='values', on_delete=models.CASCADE)
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
obj_id = models.PositiveIntegerField()
obj = GenericForeignKey(
ct_field='obj_type',
fk_field='obj_id'
)
serialized_value = models.CharField(
max_length=255
)
obj = GenericForeignKey('obj_type', 'obj_id')
serialized_value = models.CharField(max_length=255)
class Meta:
ordering = ['obj_type', 'obj_id']
@@ -270,19 +146,10 @@ class CustomFieldValue(models.Model):
@python_2_unicode_compatible
class CustomFieldChoice(models.Model):
field = models.ForeignKey(
to='extras.CustomField',
on_delete=models.CASCADE,
related_name='choices',
limit_choices_to={'type': CF_TYPE_SELECT}
)
value = models.CharField(
max_length=100
)
weight = models.PositiveSmallIntegerField(
default=100,
help_text='Higher weights appear lower in the list'
)
field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
on_delete=models.CASCADE)
value = models.CharField(max_length=100)
weight = models.PositiveSmallIntegerField(default=100, help_text="Higher weights appear lower in the list")
class Meta:
ordering = ['field', 'weight', 'value']
@@ -308,24 +175,11 @@ class CustomFieldChoice(models.Model):
@python_2_unicode_compatible
class Graph(models.Model):
type = models.PositiveSmallIntegerField(
choices=GRAPH_TYPE_CHOICES
)
weight = models.PositiveSmallIntegerField(
default=1000
)
name = models.CharField(
max_length=100,
verbose_name='Name'
)
source = models.CharField(
max_length=500,
verbose_name='Source URL'
)
link = models.URLField(
blank=True,
verbose_name='Link URL'
)
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
weight = models.PositiveSmallIntegerField(default=1000)
name = models.CharField(max_length=100, verbose_name='Name')
source = models.CharField(max_length=500, verbose_name='Source URL')
link = models.URLField(verbose_name='Link URL', blank=True)
class Meta:
ordering = ['type', 'weight', 'name']
@@ -351,26 +205,13 @@ class Graph(models.Model):
@python_2_unicode_compatible
class ExportTemplate(models.Model):
content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}
)
name = models.CharField(
max_length=100
)
description = models.CharField(
max_length=200,
blank=True
ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}, on_delete=models.CASCADE
)
name = models.CharField(max_length=100)
description = models.CharField(max_length=200, blank=True)
template_code = models.TextField()
mime_type = models.CharField(
max_length=15,
blank=True
)
file_extension = models.CharField(
max_length=15,
blank=True
)
mime_type = models.CharField(max_length=15, blank=True)
file_extension = models.CharField(max_length=15, blank=True)
class Meta:
ordering = ['content_type', 'name']
@@ -381,25 +222,19 @@ class ExportTemplate(models.Model):
def __str__(self):
return '{}: {}'.format(self.content_type, self.name)
def render_to_response(self, queryset):
def to_response(self, context_dict, filename):
"""
Render the template to an HTTP response, delivered as a named file attachment
"""
template = Template(self.template_code)
mime_type = 'text/plain' if not self.mime_type else self.mime_type
output = template.render(Context({'queryset': queryset}))
output = template.render(Context(context_dict))
# Replace CRLF-style line terminators
output = output.replace('\r\n', '\n')
# Build the response
response = HttpResponse(output, content_type=mime_type)
filename = 'netbox_{}{}'.format(
queryset.model._meta.verbose_name_plural,
'.{}'.format(self.file_extension) if self.file_extension else ''
)
if self.file_extension:
filename += '.{}'.format(self.file_extension)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response
@@ -409,35 +244,15 @@ class ExportTemplate(models.Model):
@python_2_unicode_compatible
class TopologyMap(models.Model):
name = models.CharField(
max_length=50,
unique=True
)
slug = models.SlugField(
unique=True
)
type = models.PositiveSmallIntegerField(
choices=TOPOLOGYMAP_TYPE_CHOICES,
default=TOPOLOGYMAP_TYPE_NETWORK
)
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='topology_maps',
blank=True,
null=True
)
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True, on_delete=models.CASCADE)
device_patterns = models.TextField(
help_text='Identify devices to include in the diagram using regular '
'expressions, one per line. Each line will result in a new '
'tier of the drawing. Separate multiple regexes within a '
'line using semicolons. Devices will be rendered in the '
'order they are defined.'
)
description = models.CharField(
max_length=100,
blank=True
help_text="Identify devices to include in the diagram using regular expressions, one per line. Each line will "
"result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. "
"Devices will be rendered in the order they are defined."
)
description = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['name']
@@ -453,26 +268,22 @@ class TopologyMap(models.Model):
def render(self, img_format='png'):
from dcim.models import Device
from circuits.models import CircuitTermination
from dcim.models import CONNECTION_STATUS_CONNECTED, Device, InterfaceConnection
# Construct the graph
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
G = graphviz.Graph
else:
G = graphviz.Digraph
self.graph = G()
self.graph.graph_attr['ranksep'] = '1'
graph = graphviz.Graph()
graph.graph_attr['ranksep'] = '1'
seen = set()
for i, device_set in enumerate(self.device_sets):
subgraph = G(name='sg{}'.format(i))
subgraph = graphviz.Graph(name='sg{}'.format(i))
subgraph.graph_attr['rank'] = 'same'
subgraph.graph_attr['directed'] = 'true'
# Add a pseudonode for each device_set to enforce hierarchical layout
subgraph.node('set{}'.format(i), label='', shape='none', width='0')
if i:
self.graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
# Add each device to the graph
devices = []
@@ -490,64 +301,31 @@ class TopologyMap(models.Model):
for j in range(0, len(devices) - 1):
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
self.graph.subgraph(subgraph)
graph.subgraph(subgraph)
# Compile list of all devices
device_superset = Q()
for device_set in self.device_sets:
for query in device_set.split(';'): # Split regexes on semicolons
device_superset = device_superset | Q(name__regex=query)
devices = Device.objects.filter(*(device_superset,))
# Draw edges depending on graph type
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
self.add_network_connections(devices)
elif self.type == TOPOLOGYMAP_TYPE_CONSOLE:
self.add_console_connections(devices)
elif self.type == TOPOLOGYMAP_TYPE_POWER:
self.add_power_connections(devices)
return self.graph.pipe(format=img_format)
def add_network_connections(self, devices):
from circuits.models import CircuitTermination
from dcim.models import InterfaceConnection
# Add all interface connections to the graph
devices = Device.objects.filter(*(device_superset,))
connections = InterfaceConnection.objects.filter(
interface_a__device__in=devices, interface_b__device__in=devices
)
for c in connections:
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
self.graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
# Add all circuits to the graph
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
peer_termination = termination.get_peer_termination()
if (peer_termination is not None and peer_termination.interface is not None and
peer_termination.interface.device in devices):
self.graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
def add_console_connections(self, devices):
from dcim.models import ConsolePort
# Add all console connections to the graph
console_ports = ConsolePort.objects.filter(device__in=devices, cs_port__device__in=devices)
for cp in console_ports:
style = 'solid' if cp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
self.graph.edge(cp.cs_port.device.name, cp.device.name, style=style)
def add_power_connections(self, devices):
from dcim.models import PowerPort
# Add all power connections to the graph
power_ports = PowerPort.objects.filter(device__in=devices, power_outlet__device__in=devices)
for pp in power_ports:
style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
self.graph.edge(pp.power_outlet.device.name, pp.device.name, style=style)
return graph.pipe(format=img_format)
#
@@ -573,29 +351,14 @@ class ImageAttachment(models.Model):
"""
An uploaded image which is associated with an object.
"""
content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE
)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
parent = GenericForeignKey(
ct_field='content_type',
fk_field='object_id'
)
image = models.ImageField(
upload_to=image_upload,
height_field='image_height',
width_field='image_width'
)
parent = GenericForeignKey('content_type', 'object_id')
image = models.ImageField(upload_to=image_upload, height_field='image_height', width_field='image_width')
image_height = models.PositiveSmallIntegerField()
image_width = models.PositiveSmallIntegerField()
name = models.CharField(
max_length=50,
blank=True
)
created = models.DateTimeField(
auto_now_add=True
)
name = models.CharField(max_length=50, blank=True)
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['name']
@@ -630,87 +393,6 @@ class ImageAttachment(models.Model):
return None
#
# Config contexts
#
class ConfigContext(models.Model):
"""
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
will be available to a Device in site A assigned to tenant B. Data is stored in JSON format.
"""
name = models.CharField(
max_length=100,
unique=True
)
weight = models.PositiveSmallIntegerField(
default=1000
)
description = models.CharField(
max_length=100,
blank=True
)
is_active = models.BooleanField(
default=True,
)
regions = models.ManyToManyField(
to='dcim.Region',
related_name='+',
blank=True
)
sites = models.ManyToManyField(
to='dcim.Site',
related_name='+',
blank=True
)
roles = models.ManyToManyField(
to='dcim.DeviceRole',
related_name='+',
blank=True
)
platforms = models.ManyToManyField(
to='dcim.Platform',
related_name='+',
blank=True
)
tenants = models.ManyToManyField(
to='tenancy.Tenant',
related_name='+',
blank=True
)
data = JSONField()
objects = ConfigContextQuerySet.as_manager()
class Meta:
ordering = ['weight', 'name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('extras:configcontext', kwargs={'pk': self.pk})
class ConfigContextModel(models.Model):
class Meta:
abstract = True
def get_config_context(self):
"""
Return the rendered configuration context for a device or VM.
"""
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
data = OrderedDict()
for context in ConfigContext.objects.get_for_object(self):
data.update(context.data)
return data
#
# Report results
#
@@ -719,20 +401,9 @@ 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(
to=User,
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
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()
@@ -740,115 +411,6 @@ class ReportResult(models.Model):
ordering = ['report']
#
# Change logging
#
@python_2_unicode_compatible
class ObjectChange(models.Model):
"""
Record a change to an object and the user account associated with that change. A change record may optionally
indicate an object related to the one being changed. For example, a change to an interface may also indicate the
parent device. This will ensure changes made to component models appear in the parent model's changelog.
"""
time = models.DateTimeField(
auto_now_add=True,
editable=False
)
user = models.ForeignKey(
to=User,
on_delete=models.SET_NULL,
related_name='changes',
blank=True,
null=True
)
user_name = models.CharField(
max_length=150,
editable=False
)
request_id = models.UUIDField(
editable=False
)
action = models.PositiveSmallIntegerField(
choices=OBJECTCHANGE_ACTION_CHOICES
)
changed_object_type = models.ForeignKey(
to=ContentType,
on_delete=models.PROTECT,
related_name='+'
)
changed_object_id = models.PositiveIntegerField()
changed_object = GenericForeignKey(
ct_field='changed_object_type',
fk_field='changed_object_id'
)
related_object_type = models.ForeignKey(
to=ContentType,
on_delete=models.PROTECT,
related_name='+',
blank=True,
null=True
)
related_object_id = models.PositiveIntegerField(
blank=True,
null=True
)
related_object = GenericForeignKey(
ct_field='related_object_type',
fk_field='related_object_id'
)
object_repr = models.CharField(
max_length=200,
editable=False
)
object_data = JSONField(
editable=False
)
serializer = 'extras.api.serializers.ObjectChangeSerializer'
csv_headers = [
'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
'related_object_type', 'related_object_id', 'object_repr', 'object_data',
]
class Meta:
ordering = ['-time']
def __str__(self):
return '{} {} {} by {}'.format(
self.changed_object_type,
self.object_repr,
self.get_action_display().lower(),
self.user_name
)
def save(self, *args, **kwargs):
# Record the user's name and the object's representation as static strings
self.user_name = self.user.username
self.object_repr = str(self.changed_object)
return super(ObjectChange, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('extras:objectchange', args=[self.pk])
def to_csv(self):
return (
self.time,
self.user,
self.user_name,
self.request_id,
self.get_action_display(),
self.changed_object_type,
self.changed_object_id,
self.related_object_type,
self.related_object_id,
self.object_repr,
self.object_data,
)
#
# User actions
#
@@ -901,29 +463,12 @@ class UserAction(models.Model):
"""
A record of an action (add, edit, or delete) performed on an object by a User.
"""
time = models.DateTimeField(
auto_now_add=True,
editable=False
)
user = models.ForeignKey(
to=User,
on_delete=models.CASCADE,
related_name='actions'
)
content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE
)
object_id = models.PositiveIntegerField(
blank=True,
null=True
)
action = models.PositiveSmallIntegerField(
choices=ACTION_CHOICES
)
message = models.TextField(
blank=True
)
time = models.DateTimeField(auto_now_add=True, editable=False)
user = models.ForeignKey(User, related_name='actions', on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(blank=True, null=True)
action = models.PositiveSmallIntegerField(choices=ACTION_CHOICES)
message = models.TextField(blank=True)
objects = UserActionManager()

View File

@@ -1,23 +0,0 @@
from __future__ import unicode_literals
from django.db.models import Q, QuerySet
class ConfigContextQuerySet(QuerySet):
def get_for_object(self, obj):
"""
Return all applicable ConfigContexts for a given object. Only active ConfigContexts will be included.
"""
# `device_role` for Device; `role` for VirtualMachine
role = getattr(obj, 'device_role', None) or obj.role
return self.filter(
Q(regions=getattr(obj.site, 'region', None)) | Q(regions=None),
Q(sites=obj.site) | Q(sites=None),
Q(roles=role) | Q(roles=None),
Q(tenants=obj.tenant) | Q(tenants=None),
Q(platforms=obj.platform) | Q(platforms=None),
is_active=True,
).order_by('weight', 'name')

View File

@@ -1,9 +1,8 @@
from __future__ import unicode_literals
from collections import OrderedDict
import importlib
import inspect
import pkgutil
from collections import OrderedDict
from django.conf import settings
from django.utils import timezone

View File

@@ -1,11 +1,11 @@
from __future__ import unicode_literals
import re
import time
from ncclient import manager
import paramiko
import xmltodict
from ncclient import manager
CONNECT_TIMEOUT = 5 # seconds
@@ -163,8 +163,8 @@ class IOSSSH(SSHClient):
sh_ver = self._send('show version').split('\r\n')
return {
'serial': parse(sh_ver, r'Processor board ID ([^\s]+)'),
'description': parse(sh_ver, r'cisco ([^\s]+)')
'serial': parse(sh_ver, 'Processor board ID ([^\s]+)'),
'description': parse(sh_ver, 'cisco ([^\s]+)')
}
def items(chassis_serial=None):
@@ -172,9 +172,9 @@ class IOSSSH(SSHClient):
for i in cmd:
i_fmt = i.replace('\r\n', ' ')
try:
m_name = re.search(r'NAME: "([^"]+)"', i_fmt).group(1)
m_pid = re.search(r'PID: ([^\s]+)', i_fmt).group(1)
m_serial = re.search(r'SN: ([^\s]+)', i_fmt).group(1)
m_name = re.search('NAME: "([^"]+)"', i_fmt).group(1)
m_pid = re.search('PID: ([^\s]+)', i_fmt).group(1)
m_serial = re.search('SN: ([^\s]+)', i_fmt).group(1)
# Omit built-in items and those with no PID
if m_serial != chassis_serial and m_pid.lower() != 'unspecified':
yield {
@@ -208,7 +208,7 @@ class OpengearSSH(SSHClient):
try:
stdin, stdout, stderr = self.ssh.exec_command("showserial")
serial = stdout.readlines()[0].strip()
except Exception:
except:
raise RuntimeError("Failed to glean chassis serial from device.")
# Older models don't provide serial info
if serial == "No serial number information available":
@@ -217,7 +217,7 @@ class OpengearSSH(SSHClient):
try:
stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
description = stdout.readlines()[0].split(' ', 1)[1].strip()
except Exception:
except:
raise RuntimeError("Failed to glean chassis description from device.")
return {

View File

@@ -1,107 +0,0 @@
from __future__ import unicode_literals
import django_tables2 as tables
from taggit.models import Tag
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
from .models import ConfigContext, ObjectChange
TAG_ACTIONS = """
{% if perms.taggit.change_tag %}
<a href="{% url 'extras:tag_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
{% if perms.taggit.delete_tag %}
<a href="{% url 'extras:tag_delete' slug=record.slug %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-trash" aria-hidden="true"></i></a>
{% endif %}
"""
CONFIGCONTEXT_ACTIONS = """
{% if perms.extras.change_configcontext %}
<a href="{% url 'extras:configcontext_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
{% if perms.extras.delete_configcontext %}
<a href="{% url 'extras:configcontext_delete' pk=record.pk %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-trash" aria-hidden="true"></i></a>
{% endif %}
"""
OBJECTCHANGE_TIME = """
<a href="{{ record.get_absolute_url }}">{{ value|date:"SHORT_DATETIME_FORMAT" }}</a>
"""
OBJECTCHANGE_ACTION = """
{% if record.action == 1 %}
<span class="label label-success">Created</span>
{% elif record.action == 2 %}
<span class="label label-primary">Updated</span>
{% elif record.action == 3 %}
<span class="label label-danger">Deleted</span>
{% endif %}
"""
OBJECTCHANGE_OBJECT = """
{% if record.action != 3 and record.changed_object.get_absolute_url %}
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
{% elif record.action != 3 and record.related_object.get_absolute_url %}
<a href="{{ record.related_object.get_absolute_url }}">{{ record.object_repr }}</a>
{% else %}
{{ record.object_repr }}
{% endif %}
"""
OBJECTCHANGE_REQUEST_ID = """
<a href="{% url 'extras:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>
"""
class TagTable(BaseTable):
pk = ToggleColumn()
actions = tables.TemplateColumn(
template_code=TAG_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = Tag
fields = ('pk', 'name', 'items', 'slug', 'actions')
class ConfigContextTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
is_active = BooleanColumn(
verbose_name='Active'
)
actions = tables.TemplateColumn(
template_code=CONFIGCONTEXT_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = ConfigContext
fields = ('pk', 'name', 'weight', 'is_active', 'description', 'actions')
class ObjectChangeTable(BaseTable):
time = tables.TemplateColumn(
template_code=OBJECTCHANGE_TIME
)
action = tables.TemplateColumn(
template_code=OBJECTCHANGE_ACTION
)
changed_object_type = tables.Column(
verbose_name='Type'
)
object_repr = tables.TemplateColumn(
template_code=OBJECTCHANGE_OBJECT,
verbose_name='Object'
)
request_id = tables.TemplateColumn(
template_code=OBJECTCHANGE_REQUEST_ID,
verbose_name='Request ID'
)
class Meta(BaseTable.Meta):
model = ObjectChange
fields = ('time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')

View File

@@ -1,17 +1,16 @@
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.contenttypes.models import ContentType
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from taggit.models import Tag
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from extras.constants import GRAPH_TYPE_SITE
from extras.models import ConfigContext, Graph, ExportTemplate
from dcim.models import Device
from extras.models import Graph, GRAPH_TYPE_SITE, ExportTemplate
from users.models import Token
from utilities.testing import HttpStatusMixin
from utilities.tests import HttpStatusMixin
class GraphTest(HttpStatusMixin, APITestCase):
@@ -55,7 +54,7 @@ class GraphTest(HttpStatusMixin, APITestCase):
}
url = reverse('extras-api:graph-list')
response = self.client.post(url, data, format='json', **self.header)
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Graph.objects.count(), 4)
@@ -64,35 +63,6 @@ class GraphTest(HttpStatusMixin, APITestCase):
self.assertEqual(graph4.name, data['name'])
self.assertEqual(graph4.source, data['source'])
def test_create_graph_bulk(self):
data = [
{
'type': GRAPH_TYPE_SITE,
'name': 'Test Graph 4',
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=4',
},
{
'type': GRAPH_TYPE_SITE,
'name': 'Test Graph 5',
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=5',
},
{
'type': GRAPH_TYPE_SITE,
'name': 'Test Graph 6',
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=6',
},
]
url = reverse('extras-api:graph-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Graph.objects.count(), 6)
self.assertEqual(response.data[0]['name'], data[0]['name'])
self.assertEqual(response.data[1]['name'], data[1]['name'])
self.assertEqual(response.data[2]['name'], data[2]['name'])
def test_update_graph(self):
data = {
@@ -102,7 +72,7 @@ class GraphTest(HttpStatusMixin, APITestCase):
}
url = reverse('extras-api:graph-detail', kwargs={'pk': self.graph1.pk})
response = self.client.put(url, data, format='json', **self.header)
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(Graph.objects.count(), 3)
@@ -165,7 +135,7 @@ class ExportTemplateTest(HttpStatusMixin, APITestCase):
}
url = reverse('extras-api:exporttemplate-list')
response = self.client.post(url, data, format='json', **self.header)
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(ExportTemplate.objects.count(), 4)
@@ -174,35 +144,6 @@ class ExportTemplateTest(HttpStatusMixin, APITestCase):
self.assertEqual(exporttemplate4.name, data['name'])
self.assertEqual(exporttemplate4.template_code, data['template_code'])
def test_create_exporttemplate_bulk(self):
data = [
{
'content_type': self.content_type.pk,
'name': 'Test Export Template 4',
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
},
{
'content_type': self.content_type.pk,
'name': 'Test Export Template 5',
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
},
{
'content_type': self.content_type.pk,
'name': 'Test Export Template 6',
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
},
]
url = reverse('extras-api:exporttemplate-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(ExportTemplate.objects.count(), 6)
self.assertEqual(response.data[0]['name'], data[0]['name'])
self.assertEqual(response.data[1]['name'], data[1]['name'])
self.assertEqual(response.data[2]['name'], data[2]['name'])
def test_update_exporttemplate(self):
data = {
@@ -212,7 +153,7 @@ class ExportTemplateTest(HttpStatusMixin, APITestCase):
}
url = reverse('extras-api:exporttemplate-detail', kwargs={'pk': self.exporttemplate1.pk})
response = self.client.put(url, data, format='json', **self.header)
response = self.client.put(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(ExportTemplate.objects.count(), 3)
@@ -227,273 +168,3 @@ class ExportTemplateTest(HttpStatusMixin, APITestCase):
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(ExportTemplate.objects.count(), 2)
class TagTest(HttpStatusMixin, APITestCase):
def setUp(self):
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
self.tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
self.tag2 = Tag.objects.create(name='Test Tag 2', slug='test-tag-2')
self.tag3 = Tag.objects.create(name='Test Tag 3', slug='test-tag-3')
def test_get_tag(self):
url = reverse('extras-api:tag-detail', kwargs={'pk': self.tag1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['name'], self.tag1.name)
def test_list_tags(self):
url = reverse('extras-api:tag-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3)
def test_create_tag(self):
data = {
'name': 'Test Tag 4',
'slug': 'test-tag-4',
}
url = reverse('extras-api:tag-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Tag.objects.count(), 4)
tag4 = Tag.objects.get(pk=response.data['id'])
self.assertEqual(tag4.name, data['name'])
self.assertEqual(tag4.slug, data['slug'])
def test_create_tag_bulk(self):
data = [
{
'name': 'Test Tag 4',
'slug': 'test-tag-4',
},
{
'name': 'Test Tag 5',
'slug': 'test-tag-5',
},
{
'name': 'Test Tag 6',
'slug': 'test-tag-6',
},
]
url = reverse('extras-api:tag-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Tag.objects.count(), 6)
self.assertEqual(response.data[0]['name'], data[0]['name'])
self.assertEqual(response.data[1]['name'], data[1]['name'])
self.assertEqual(response.data[2]['name'], data[2]['name'])
def test_update_tag(self):
data = {
'name': 'Test Tag X',
'slug': 'test-tag-x',
}
url = reverse('extras-api:tag-detail', kwargs={'pk': self.tag1.pk})
response = self.client.put(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(Tag.objects.count(), 3)
tag1 = Tag.objects.get(pk=response.data['id'])
self.assertEqual(tag1.name, data['name'])
self.assertEqual(tag1.slug, data['slug'])
def test_delete_tag(self):
url = reverse('extras-api:tag-detail', kwargs={'pk': self.tag1.pk})
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(Tag.objects.count(), 2)
class ConfigContextTest(HttpStatusMixin, APITestCase):
def setUp(self):
user = User.objects.create(username='testuser', is_superuser=True)
token = Token.objects.create(user=user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
self.configcontext1 = ConfigContext.objects.create(
name='Test Config Context 1',
weight=100,
data={'foo': 123}
)
self.configcontext2 = ConfigContext.objects.create(
name='Test Config Context 2',
weight=200,
data={'bar': 456}
)
self.configcontext3 = ConfigContext.objects.create(
name='Test Config Context 3',
weight=300,
data={'baz': 789}
)
def test_get_configcontext(self):
url = reverse('extras-api:configcontext-detail', kwargs={'pk': self.configcontext1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['name'], self.configcontext1.name)
self.assertEqual(response.data['data'], self.configcontext1.data)
def test_list_configcontexts(self):
url = reverse('extras-api:configcontext-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3)
def test_create_configcontext(self):
data = {
'name': 'Test Config Context 4',
'weight': 1000,
'data': {'foo': 'XXX'}
}
url = reverse('extras-api:configcontext-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(ConfigContext.objects.count(), 4)
configcontext4 = ConfigContext.objects.get(pk=response.data['id'])
self.assertEqual(configcontext4.name, data['name'])
self.assertEqual(configcontext4.data, data['data'])
def test_create_configcontext_bulk(self):
data = [
{
'name': 'Test Config Context 4',
'data': {'more_foo': True},
},
{
'name': 'Test Config Context 5',
'data': {'more_bar': False},
},
{
'name': 'Test Config Context 6',
'data': {'more_baz': None},
},
]
url = reverse('extras-api:configcontext-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(ConfigContext.objects.count(), 6)
for i in range(0, 3):
self.assertEqual(response.data[i]['name'], data[i]['name'])
self.assertEqual(response.data[i]['data'], data[i]['data'])
def test_update_configcontext(self):
data = {
'name': 'Test Config Context X',
'weight': 999,
'data': {'foo': 'XXX'}
}
url = reverse('extras-api:configcontext-detail', kwargs={'pk': self.configcontext1.pk})
response = self.client.put(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(ConfigContext.objects.count(), 3)
configcontext1 = ConfigContext.objects.get(pk=response.data['id'])
self.assertEqual(configcontext1.name, data['name'])
self.assertEqual(configcontext1.weight, data['weight'])
self.assertEqual(configcontext1.data, data['data'])
def test_delete_configcontext(self):
url = reverse('extras-api:configcontext-detail', kwargs={'pk': self.configcontext1.pk})
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(ConfigContext.objects.count(), 2)
def test_render_configcontext_for_object(self):
# Create a Device for which we'll render a config context
manufacturer = Manufacturer.objects.create(
name='Test Manufacturer',
slug='test-manufacturer'
)
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='Test Device Type'
)
device_role = DeviceRole.objects.create(
name='Test Role',
slug='test-role'
)
site = Site.objects.create(
name='Test Site',
slug='test-site'
)
device = Device.objects.create(
name='Test Device',
device_type=device_type,
device_role=device_role,
site=site
)
# Test default config contexts (created at test setup)
rendered_context = device.get_config_context()
self.assertEqual(rendered_context['foo'], 123)
self.assertEqual(rendered_context['bar'], 456)
self.assertEqual(rendered_context['baz'], 789)
# Add another context specific to the site
configcontext4 = ConfigContext(
name='Test Config Context 4',
data={'site_data': 'ABC'}
)
configcontext4.save()
configcontext4.sites.add(site)
rendered_context = device.get_config_context()
self.assertEqual(rendered_context['site_data'], 'ABC')
# Override one of the default contexts
configcontext5 = ConfigContext(
name='Test Config Context 5',
weight=2000,
data={'foo': 999}
)
configcontext5.save()
configcontext5.sites.add(site)
rendered_context = device.get_config_context()
self.assertEqual(rendered_context['foo'], 999)
# Add a context which does NOT match our device and ensure it does not apply
site2 = Site.objects.create(
name='Test Site 2',
slug='test-site-2'
)
configcontext6 = ConfigContext(
name='Test Config Context 6',
weight=2000,
data={'bar': 999}
)
configcontext6.save()
configcontext6.sites.add(site2)
rendered_context = device.get_config_context()
self.assertEqual(rendered_context['bar'], 456)

View File

@@ -1,19 +1,21 @@
from __future__ import unicode_literals
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.contenttypes.models import ContentType
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
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 CustomField, CustomFieldValue, CustomFieldChoice
from extras.models import (
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 utilities.testing import HttpStatusMixin
from utilities.tests import HttpStatusMixin
class CustomFieldTest(TestCase):
@@ -45,7 +47,7 @@ class CustomFieldTest(TestCase):
# Create a custom field
cf = CustomField(type=data['field_type'], name='my_field', required=False)
cf.save()
cf.obj_type.set([obj_type])
cf.obj_type = [obj_type]
cf.save()
# Assign a value to the first Site
@@ -73,7 +75,7 @@ class CustomFieldTest(TestCase):
# Create a custom field
cf = CustomField(type=CF_TYPE_SELECT, name='my_field', required=False)
cf.save()
cf.obj_type.set([obj_type])
cf.obj_type = [obj_type]
cf.save()
# Create some choices for the field
@@ -115,37 +117,37 @@ class CustomFieldAPITest(HttpStatusMixin, APITestCase):
# Text custom field
self.cf_text = CustomField(type=CF_TYPE_TEXT, name='magic_word')
self.cf_text.save()
self.cf_text.obj_type.set([content_type])
self.cf_text.obj_type = [content_type]
self.cf_text.save()
# Integer custom field
self.cf_integer = CustomField(type=CF_TYPE_INTEGER, name='magic_number')
self.cf_integer.save()
self.cf_integer.obj_type.set([content_type])
self.cf_integer.obj_type = [content_type]
self.cf_integer.save()
# Boolean custom field
self.cf_boolean = CustomField(type=CF_TYPE_BOOLEAN, name='is_magic')
self.cf_boolean.save()
self.cf_boolean.obj_type.set([content_type])
self.cf_boolean.obj_type = [content_type]
self.cf_boolean.save()
# Date custom field
self.cf_date = CustomField(type=CF_TYPE_DATE, name='magic_date')
self.cf_date.save()
self.cf_date.obj_type.set([content_type])
self.cf_date.obj_type = [content_type]
self.cf_date.save()
# URL custom field
self.cf_url = CustomField(type=CF_TYPE_URL, name='magic_url')
self.cf_url.save()
self.cf_url.obj_type.set([content_type])
self.cf_url.obj_type = [content_type]
self.cf_url.save()
# Select custom field
self.cf_select = CustomField(type=CF_TYPE_SELECT, name='magic_choice')
self.cf_select.save()
self.cf_select.obj_type.set([content_type])
self.cf_select.obj_type = [content_type]
self.cf_select.save()
self.cf_select_choice1 = CustomFieldChoice(field=self.cf_select, value='Foo')
self.cf_select_choice1.save()

View File

@@ -4,23 +4,10 @@ from django.conf.urls import url
from extras import views
app_name = 'extras'
urlpatterns = [
# Tags
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
# Config contexts
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
# Image attachments
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'),
@@ -30,8 +17,4 @@ urlpatterns = [
url(r'^reports/(?P<name>[^/]+\.[^/]+)/$', views.ReportView.as_view(), name='report'),
url(r'^reports/(?P<name>[^/]+\.[^/]+)/run/$', views.ReportRunView.as_view(), name='report_run'),
# Change logging
url(r'^changelog/$', views.ObjectChangeListView.as_view(), name='objectchange_list'),
url(r'^changelog/(?P<pk>\d+)/$', views.ObjectChangeView.as_view(), name='objectchange'),
]

View File

@@ -1,190 +1,17 @@
from __future__ import unicode_literals
from django import template
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Q
from django.contrib import messages
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.safestring import mark_safe
from django.views.generic import View
from taggit.models import Tag
from utilities.forms import ConfirmationForm
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
from . import filters
from .forms import ConfigContextForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
from utilities.views import ObjectDeleteView, ObjectEditView
from .forms import ImageAttachmentForm
from .models import ImageAttachment, ReportResult, UserAction
from .reports import get_report, get_reports
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
#
# Tags
#
class TagListView(ObjectListView):
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
table = TagTable
template_name = 'extras/tag_list.html'
class TagEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'taggit.change_tag'
model = Tag
model_form = TagForm
default_return_url = 'extras:tag_list'
class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'taggit.delete_tag'
model = Tag
default_return_url = 'extras:tag_list'
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype'
cls = Tag
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
table = TagTable
default_return_url = 'extras:tag_list'
#
# Config contexts
#
class ConfigContextListView(ObjectListView):
queryset = ConfigContext.objects.all()
table = ConfigContextTable
template_name = 'extras/configcontext_list.html'
class ConfigContextView(View):
def get(self, request, pk):
configcontext = get_object_or_404(ConfigContext, pk=pk)
return render(request, 'extras/configcontext.html', {
'configcontext': configcontext,
})
class ConfigContextCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'extras.add_configcontext'
model = ConfigContext
model_form = ConfigContextForm
default_return_url = 'extras:configcontext_list'
template_name = 'extras/configcontext_edit.html'
class ConfigContextEditView(ConfigContextCreateView):
permission_required = 'extras.change_configcontext'
class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'extras.delete_configcontext'
model = ConfigContext
default_return_url = 'extras:configcontext_list'
class ConfigContextBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'extras.delete_cconfigcontext'
cls = ConfigContext
queryset = ConfigContext.objects.all()
table = ConfigContextTable
default_return_url = 'extras:configcontext_list'
class ObjectConfigContextView(View):
object_class = None
base_template = None
def get(self, request, pk):
obj = get_object_or_404(self.object_class, pk=pk)
source_contexts = ConfigContext.objects.get_for_object(obj)
return render(request, 'extras/object_configcontext.html', {
self.object_class._meta.model_name: obj,
'rendered_context': obj.get_config_context(),
'source_contexts': source_contexts,
'base_template': self.base_template,
'active_tab': 'config-context',
})
#
# Change logging
#
class ObjectChangeListView(ObjectListView):
queryset = ObjectChange.objects.select_related('user', 'changed_object_type')
filter = filters.ObjectChangeFilter
filter_form = ObjectChangeFilterForm
table = ObjectChangeTable
template_name = 'extras/objectchange_list.html'
class ObjectChangeView(View):
def get(self, request, pk):
objectchange = get_object_or_404(ObjectChange, pk=pk)
related_changes = ObjectChange.objects.filter(request_id=objectchange.request_id).exclude(pk=objectchange.pk)
related_changes_table = ObjectChangeTable(
data=related_changes[:50],
orderable=False
)
return render(request, 'extras/objectchange.html', {
'objectchange': objectchange,
'related_changes_table': related_changes_table,
'related_changes_count': related_changes.count()
})
class ObjectChangeLogView(View):
"""
Present a history of changes made to a particular object.
"""
def get(self, request, model, **kwargs):
# Get object my model and kwargs (e.g. slug='foo')
obj = get_object_or_404(model, **kwargs)
# Gather all changes for this object (and its related objects)
content_type = ContentType.objects.get_for_model(model)
objectchanges = ObjectChange.objects.select_related(
'user', 'changed_object_type'
).filter(
Q(changed_object_type=content_type, changed_object_id=obj.pk) |
Q(related_object_type=content_type, related_object_id=obj.pk)
)
objectchanges_table = ObjectChangeTable(
data=objectchanges,
orderable=False
)
# Check whether a header template exists for this model
base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name)
try:
template.loader.get_template(base_template)
object_var = model._meta.model_name
except template.TemplateDoesNotExist:
base_template = '_base.html'
object_var = 'obj'
return render(request, 'extras/object_changelog.html', {
object_var: obj,
'objectchanges_table': objectchanges_table,
'base_template': base_template,
'active_tab': 'changelog',
})
#
@@ -286,5 +113,6 @@ class ReportRunView(PermissionRequiredMixin, View):
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,119 +0,0 @@
import time
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.db.models.signals import post_save, post_delete
from django.dispatch import Signal
from extras.models import Webhook
from utilities.utils import dynamic_import
def enqueue_webhooks(webhooks, model_class, data, event, signal_received_timestamp):
"""
Serialize data and enqueue webhooks
"""
serializer_context = {
'request': None,
}
if isinstance(data, list):
serializer_property = data[0].serializer
serializer_cls = dynamic_import(serializer_property)
serialized_data = serializer_cls(data, context=serializer_context, many=True)
else:
serializer_property = data.serializer
serializer_cls = dynamic_import(serializer_property)
serialized_data = serializer_cls(data, context=serializer_context)
from django_rq import get_queue
webhook_queue = get_queue('default')
for webhook in webhooks:
webhook_queue.enqueue("extras.webhooks_worker.process_webhook",
webhook,
serialized_data.data,
model_class,
event,
signal_received_timestamp)
def post_save_receiver(sender, instance, created, **kwargs):
"""
Receives post_save signals from registered models. If the webhook
backend is enabled, queue any webhooks that apply to the event.
"""
if settings.WEBHOOKS_ENABLED:
signal_received_timestamp = time.time()
# look for any webhooks that match this event
updated = not created
obj_type = ContentType.objects.get_for_model(sender)
webhooks = Webhook.objects.filter(
Q(enabled=True) &
(
Q(type_create=created) |
Q(type_update=updated)
) &
Q(obj_type=obj_type)
)
event = 'created' if created else 'updated'
if webhooks:
enqueue_webhooks(webhooks, sender, instance, event, signal_received_timestamp)
def post_delete_receiver(sender, instance, **kwargs):
"""
Receives post_delete signals from registered models. If the webhook
backend is enabled, queue any webhooks that apply to the event.
"""
if settings.WEBHOOKS_ENABLED:
signal_received_timestamp = time.time()
obj_type = ContentType.objects.get_for_model(sender)
# look for any webhooks that match this event
webhooks = Webhook.objects.filter(enabled=True, type_delete=True, obj_type=obj_type)
if webhooks:
enqueue_webhooks(webhooks, sender, instance, 'deleted', signal_received_timestamp)
def bulk_operation_receiver(sender, **kwargs):
"""
Receives bulk_operation_signal signals from registered models. If the webhook
backend is enabled, queue any webhooks that apply to the event.
"""
if settings.WEBHOOKS_ENABLED:
signal_received_timestamp = time.time()
event = kwargs['event']
obj_type = ContentType.objects.get_for_model(sender)
# look for any webhooks that match this event
if event == 'created':
webhooks = Webhook.objects.filter(enabled=True, type_create=True, obj_type=obj_type)
elif event == 'updated':
webhooks = Webhook.objects.filter(enabled=True, type_update=True, obj_type=obj_type)
elif event == 'deleted':
webhooks = Webhook.objects.filter(enabled=True, type_delete=True, obj_type=obj_type)
else:
webhooks = None
if webhooks:
enqueue_webhooks(webhooks, sender, list(kwargs['instances']), event, signal_received_timestamp)
# the bulk operation signal is used to overcome signals not being sent for bulk model changes
bulk_operation_signal = Signal(providing_args=["instances", "event"])
bulk_operation_signal.connect(bulk_operation_receiver)
def register_signals(senders):
"""
Take a list of senders (Models) and register them to the post_save
and post_delete signal receivers.
"""
if settings.WEBHOOKS_ENABLED:
# only register signals if the backend is enabled
# this reduces load by not firing signals if the
# webhook backend feature is disabled
for sender in senders:
post_save.connect(post_save_receiver, sender=sender)
post_delete.connect(post_delete_receiver, sender=sender)

View File

@@ -1,51 +0,0 @@
import hashlib
import hmac
import requests
from django_rq import job
from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED
@job('default')
def process_webhook(webhook, data, model_class, event, timestamp):
"""
Make a POST request to the defined Webhook
"""
payload = {
'event': event,
'timestamp': timestamp,
'model': model_class.__name__,
'data': data
}
headers = {
'Content-Type': webhook.get_http_content_type_display(),
}
params = {
'method': 'POST',
'url': webhook.payload_url,
'headers': headers
}
if webhook.http_content_type == WEBHOOK_CT_JSON:
params.update({'json': payload})
elif webhook.http_content_type == WEBHOOK_CT_X_WWW_FORM_ENCODED:
params.update({'data': payload})
prepared_request = requests.Request(**params).prepare()
if webhook.secret != '':
# sign the request with the secret
hmac_prep = hmac.new(bytearray(webhook.secret, 'utf8'), prepared_request.body, digestmod=hashlib.sha512)
prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest()
with requests.Session() as session:
session.verify = webhook.ssl_verification
response = session.send(prepared_request)
if response.status_code >= 200 and response.status_code <= 299:
return 'Status {} returned, webhook successfully processed.'.format(response.status_code)
else:
raise requests.exceptions.RequestException(
"Status {} returned, webhook FAILED to process.".format(response.status_code)
)

View File

@@ -1,23 +1,17 @@
from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers
from rest_framework.reverse import reverse
from rest_framework.validators import UniqueTogetherValidator
from taggit.models import Tag
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
from dcim.models import Interface
from extras.api.customfields import CustomFieldModelSerializer
from ipam.constants import (
IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
from ipam.models import (
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 utilities.api import (
ChoiceFieldSerializer, SerializedPKRelatedField, TagField, ValidatedModelSerializer, WritableNestedSerializer,
)
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.api.serializers import NestedVirtualMachineSerializer
@@ -26,18 +20,14 @@ from virtualization.api.serializers import NestedVirtualMachineSerializer
#
class VRFSerializer(CustomFieldModelSerializer):
tenant = NestedTenantSerializer(required=False, allow_null=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
tenant = NestedTenantSerializer()
class Meta:
model = VRF
fields = [
'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'tags', 'display_name', 'custom_fields',
'created', 'last_updated',
]
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'display_name', 'custom_fields']
class NestedVRFSerializer(WritableNestedSerializer):
class NestedVRFSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
class Meta:
@@ -45,6 +35,13 @@ class NestedVRFSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'name', 'rd']
class WritableVRFSerializer(CustomFieldModelSerializer):
class Meta:
model = VRF
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields']
#
# Roles
#
@@ -56,7 +53,7 @@ class RoleSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'weight']
class NestedRoleSerializer(WritableNestedSerializer):
class NestedRoleSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
class Meta:
@@ -75,7 +72,7 @@ class RIRSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'is_private']
class NestedRIRSerializer(WritableNestedSerializer):
class NestedRIRSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
class Meta:
@@ -89,18 +86,13 @@ class NestedRIRSerializer(WritableNestedSerializer):
class AggregateSerializer(CustomFieldModelSerializer):
rir = NestedRIRSerializer()
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
class Meta:
model = Aggregate
fields = [
'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'tags', 'custom_fields', 'created',
'last_updated',
]
read_only_fields = ['family']
fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields']
class NestedAggregateSerializer(WritableNestedSerializer):
class NestedAggregateSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
class Meta(AggregateSerializer.Meta):
@@ -108,12 +100,34 @@ class NestedAggregateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'family', 'prefix']
class WritableAggregateSerializer(CustomFieldModelSerializer):
class Meta:
model = Aggregate
fields = ['id', 'prefix', 'rir', 'date_added', 'description', 'custom_fields']
#
# VLAN groups
#
class VLANGroupSerializer(ValidatedModelSerializer):
site = NestedSiteSerializer(required=False, allow_null=True)
class VLANGroupSerializer(serializers.ModelSerializer):
site = NestedSiteSerializer()
class Meta:
model = VLANGroup
fields = ['id', 'name', 'slug', 'site']
class NestedVLANGroupSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
class Meta:
model = VLANGroup
fields = ['id', 'url', 'name', 'slug']
class WritableVLANGroupSerializer(serializers.ModelSerializer):
class Meta:
model = VLANGroup
@@ -130,37 +144,43 @@ class VLANGroupSerializer(ValidatedModelSerializer):
validator(data)
# Enforce model validation
super(VLANGroupSerializer, self).validate(data)
super(WritableVLANGroupSerializer, self).validate(data)
return data
class NestedVLANGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
class Meta:
model = VLANGroup
fields = ['id', 'url', 'name', 'slug']
#
# VLANs
#
class VLANSerializer(CustomFieldModelSerializer):
site = NestedSiteSerializer(required=False, allow_null=True)
group = NestedVLANGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES, required=False)
role = NestedRoleSerializer(required=False, allow_null=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
site = NestedSiteSerializer()
group = NestedVLANGroupSerializer()
tenant = NestedTenantSerializer()
status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES)
role = NestedRoleSerializer()
class Meta:
model = VLAN
fields = [
'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags', 'display_name',
'custom_fields', 'created', 'last_updated',
'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name',
'custom_fields',
]
class NestedVLANSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
class Meta:
model = VLAN
fields = ['id', 'url', 'vid', 'name', 'display_name']
class WritableVLANSerializer(CustomFieldModelSerializer):
class Meta:
model = VLAN
fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'custom_fields']
validators = []
def validate(self, data):
@@ -173,42 +193,32 @@ class VLANSerializer(CustomFieldModelSerializer):
validator(data)
# Enforce model validation
super(VLANSerializer, self).validate(data)
super(WritableVLANSerializer, self).validate(data)
return data
class NestedVLANSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
class Meta:
model = VLAN
fields = ['id', 'url', 'vid', 'name', 'display_name']
#
# Prefixes
#
class PrefixSerializer(CustomFieldModelSerializer):
site = NestedSiteSerializer(required=False, allow_null=True)
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
vlan = NestedVLANSerializer(required=False, allow_null=True)
status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES, required=False)
role = NestedRoleSerializer(required=False, allow_null=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
site = NestedSiteSerializer()
vrf = NestedVRFSerializer()
tenant = NestedTenantSerializer()
vlan = NestedVLANSerializer()
status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES)
role = NestedRoleSerializer()
class Meta:
model = Prefix
fields = [
'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
'tags', 'custom_fields', 'created', 'last_updated',
'custom_fields',
]
read_only_fields = ['family']
class NestedPrefixSerializer(WritableNestedSerializer):
class NestedPrefixSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
class Meta:
@@ -216,62 +226,46 @@ class NestedPrefixSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'family', 'prefix']
class AvailablePrefixSerializer(serializers.Serializer):
class WritablePrefixSerializer(CustomFieldModelSerializer):
def to_representation(self, instance):
if self.context.get('vrf'):
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
else:
vrf = None
return OrderedDict([
('family', instance.version),
('prefix', str(instance)),
('vrf', vrf),
])
class Meta:
model = Prefix
fields = [
'id', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
'custom_fields',
]
#
# IP addresses
#
class IPAddressInterfaceSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField() # We're imitating a HyperlinkedIdentityField here
device = NestedDeviceSerializer()
class IPAddressInterfaceSerializer(InterfaceSerializer):
virtual_machine = NestedVirtualMachineSerializer()
class Meta(InterfaceSerializer.Meta):
model = Interface
fields = [
'id', 'url', 'device', 'virtual_machine', 'name',
'id', 'device', 'virtual_machine', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address',
'mgmt_only', 'description', 'is_connected', 'interface_connection', 'circuit_termination',
]
def get_url(self, obj):
"""
Return a link to the Interface via either the DCIM API if the parent is a Device, or via the virtualization API
if the parent is a VirtualMachine.
"""
url_name = 'dcim-api:interface-detail' if obj.device else 'virtualization-api:interface-detail'
return reverse(url_name, kwargs={'pk': obj.pk}, request=self.context['request'])
class IPAddressSerializer(CustomFieldModelSerializer):
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES, required=False)
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES, required=False)
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
tags = TagField(queryset=Tag.objects.all(), required=False, many=True)
vrf = NestedVRFSerializer()
tenant = NestedTenantSerializer()
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
interface = IPAddressInterfaceSerializer()
class Meta:
model = IPAddress
fields = [
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
'nat_outside', 'tags', 'custom_fields', 'created', 'last_updated',
'nat_outside', 'custom_fields',
]
read_only_fields = ['family']
class NestedIPAddressSerializer(WritableNestedSerializer):
class NestedIPAddressSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
class Meta:
@@ -279,8 +273,18 @@ class NestedIPAddressSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'family', 'address']
IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer(required=False, allow_null=True)
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer(read_only=True)
IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer()
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer()
class WritableIPAddressSerializer(CustomFieldModelSerializer):
class Meta:
model = IPAddress
fields = [
'id', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
'custom_fields',
]
class AvailableIPSerializer(serializers.Serializer):
@@ -301,20 +305,20 @@ class AvailableIPSerializer(serializers.Serializer):
# Services
#
class ServiceSerializer(CustomFieldModelSerializer):
device = NestedDeviceSerializer(required=False, allow_null=True)
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
class ServiceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
virtual_machine = NestedVirtualMachineSerializer()
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
ipaddresses = SerializedPKRelatedField(
queryset=IPAddress.objects.all(),
serializer=NestedIPAddressSerializer,
required=False,
many=True
)
ipaddresses = NestedIPAddressSerializer(many=True)
class Meta:
model = Service
fields = [
'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description',
'custom_fields', 'created', 'last_updated',
]
fields = ['id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description']
# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError.
class WritableServiceSerializer(serializers.ModelSerializer):
class Meta:
model = Service
fields = ['id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description']

View File

@@ -1,16 +1,18 @@
from __future__ import unicode_literals
from rest_framework import status
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from django.conf import settings
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from extras.api.views import CustomFieldModelViewSet
from ipam import filters
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.api import FieldChoicesViewSet, ModelViewSet
from ipam import filters
from extras.api.views import CustomFieldModelViewSet
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers
@@ -32,9 +34,10 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
# VRFs
#
class VRFViewSet(CustomFieldModelViewSet):
class VRFViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = VRF.objects.select_related('tenant')
serializer_class = serializers.VRFSerializer
write_serializer_class = serializers.WritableVRFSerializer
filter_class = filters.VRFFilter
@@ -52,9 +55,10 @@ class RIRViewSet(ModelViewSet):
# Aggregates
#
class AggregateViewSet(CustomFieldModelViewSet):
class AggregateViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Aggregate.objects.select_related('rir')
serializer_class = serializers.AggregateSerializer
write_serializer_class = serializers.WritableAggregateSerializer
filter_class = filters.AggregateFilter
@@ -72,96 +76,13 @@ class RoleViewSet(ModelViewSet):
# Prefixes
#
class PrefixViewSet(CustomFieldModelViewSet):
class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
serializer_class = serializers.PrefixSerializer
write_serializer_class = serializers.WritablePrefixSerializer
filter_class = filters.PrefixFilter
@action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
def available_prefixes(self, request, pk=None):
"""
A convenience method for returning available child prefixes within a parent.
"""
prefix = get_object_or_404(Prefix, pk=pk)
available_prefixes = prefix.get_available_prefixes()
if request.method == 'POST':
# Permissions check
if not request.user.has_perm('ipam.add_prefix'):
raise PermissionDenied()
# Normalize to a list of objects
requested_prefixes = request.data if isinstance(request.data, list) else [request.data]
# Allocate prefixes to the requested objects based on availability within the parent
for i, requested_prefix in enumerate(requested_prefixes):
# Validate requested prefix size
error_msg = None
if 'prefix_length' not in requested_prefix:
error_msg = "Item {}: prefix_length field missing".format(i)
elif not isinstance(requested_prefix['prefix_length'], int):
error_msg = "Item {}: Invalid prefix length ({})".format(
i, requested_prefix['prefix_length']
)
elif prefix.family == 4 and requested_prefix['prefix_length'] > 32:
error_msg = "Item {}: Invalid prefix length ({}) for IPv4".format(
i, requested_prefix['prefix_length']
)
elif prefix.family == 6 and requested_prefix['prefix_length'] > 128:
error_msg = "Item {}: Invalid prefix length ({}) for IPv6".format(
i, requested_prefix['prefix_length']
)
if error_msg:
return Response(
{
"detail": error_msg
},
status=status.HTTP_400_BAD_REQUEST
)
# Find the first available prefix equal to or larger than the requested size
for available_prefix in available_prefixes.iter_cidrs():
if requested_prefix['prefix_length'] >= available_prefix.prefixlen:
allocated_prefix = '{}/{}'.format(available_prefix.network, requested_prefix['prefix_length'])
requested_prefix['prefix'] = allocated_prefix
requested_prefix['vrf'] = prefix.vrf.pk if prefix.vrf else None
break
else:
return Response(
{
"detail": "Insufficient space is available to accommodate the requested prefix size(s)"
},
status=status.HTTP_400_BAD_REQUEST
)
# Remove the allocated prefix from the list of available prefixes
available_prefixes.remove(allocated_prefix)
# Initialize the serializer with a list or a single object depending on what was requested
if isinstance(request.data, list):
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True)
else:
serializer = serializers.PrefixSerializer(data=requested_prefixes[0])
# Create the new Prefix(es)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
serializer = serializers.AvailablePrefixSerializer(available_prefixes.iter_cidrs(), many=True, context={
'request': request,
'vrf': prefix.vrf,
})
return Response(serializer.data)
@action(detail=True, url_path='available-ips', methods=['get', 'post'])
@detail_route(url_path='available-ips', methods=['get', 'post'])
def available_ips(self, request, pk=None):
"""
A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
@@ -177,40 +98,28 @@ class PrefixViewSet(CustomFieldModelViewSet):
if not request.user.has_perm('ipam.add_ipaddress'):
raise PermissionDenied()
# Normalize to a list of objects
requested_ips = request.data if isinstance(request.data, list) else [request.data]
# Determine if the requested number of IPs is available
available_ips = prefix.get_available_ips()
if available_ips.size < len(requested_ips):
# Find the first available IP address in the prefix
try:
ipaddress = list(prefix.get_available_ips())[0]
except IndexError:
return Response(
{
"detail": "An insufficient number of IP addresses are available within the prefix {} ({} "
"requested, {} available)".format(prefix, len(requested_ips), len(available_ips))
"detail": "There are no available IPs within this prefix ({})".format(prefix)
},
status=status.HTTP_400_BAD_REQUEST
)
# Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
available_ips = iter(available_ips)
for requested_ip in requested_ips:
requested_ip['address'] = next(available_ips)
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
# Initialize the serializer with a list or a single object depending on what was requested
if isinstance(request.data, list):
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True)
else:
serializer = serializers.IPAddressSerializer(data=requested_ips[0])
# Create the new IP address(es)
# Create the new IP address
data = request.data.copy()
data['address'] = '{}/{}'.format(ipaddress, prefix.prefix.prefixlen)
data['vrf'] = prefix.vrf.pk if prefix.vrf else None
serializer = serializers.WritableIPAddressSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Determine the maximum number of IPs to return
# Determine the maximum amount of IPs to return
else:
try:
limit = int(request.query_params.get('limit', settings.PAGINATE_COUNT))
@@ -238,13 +147,14 @@ class PrefixViewSet(CustomFieldModelViewSet):
# IP addresses
#
class IPAddressViewSet(CustomFieldModelViewSet):
class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = IPAddress.objects.select_related(
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine'
'vrf__tenant', 'tenant', 'nat_inside'
).prefetch_related(
'nat_outside'
'interface__device', 'interface__virtual_machine'
)
serializer_class = serializers.IPAddressSerializer
write_serializer_class = serializers.WritableIPAddressSerializer
filter_class = filters.IPAddressFilter
@@ -252,9 +162,10 @@ class IPAddressViewSet(CustomFieldModelViewSet):
# VLAN groups
#
class VLANGroupViewSet(ModelViewSet):
class VLANGroupViewSet(WritableSerializerMixin, ModelViewSet):
queryset = VLANGroup.objects.select_related('site')
serializer_class = serializers.VLANGroupSerializer
write_serializer_class = serializers.WritableVLANGroupSerializer
filter_class = filters.VLANGroupFilter
@@ -262,9 +173,10 @@ class VLANGroupViewSet(ModelViewSet):
# VLANs
#
class VLANViewSet(CustomFieldModelViewSet):
class VLANViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
serializer_class = serializers.VLANSerializer
write_serializer_class = serializers.WritableVLANSerializer
filter_class = filters.VLANFilter
@@ -272,7 +184,8 @@ class VLANViewSet(CustomFieldModelViewSet):
# Services
#
class ServiceViewSet(ModelViewSet):
class ServiceViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Service.objects.select_related('device')
serializer_class = serializers.ServiceSerializer
write_serializer_class = serializers.WritableServiceSerializer
filter_class = filters.ServiceFilter

View File

@@ -6,10 +6,3 @@ from django.apps import AppConfig
class IPAMConfig(AppConfig):
name = "ipam"
verbose_name = "IPAM"
def ready(self):
# register webhook signals
from extras.webhooks import register_signals
from .models import Aggregate, Prefix, IPAddress, VLAN, VRF, VLANGroup, Service
register_signals([Aggregate, Prefix, IPAddress, VLAN, VRF, VLANGroup, Service])

View File

@@ -61,7 +61,7 @@ VLAN_STATUS_CHOICES = (
(VLAN_STATUS_DEPRECATED, 'Deprecated')
)
# Bootstrap CSS classes
# Bootstrap CSS classes for various statuses
STATUS_CHOICE_CLASSES = {
0: 'default',
1: 'primary',
@@ -70,16 +70,6 @@ STATUS_CHOICE_CLASSES = {
4: 'warning',
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_PROTOCOL_TCP = 6

View File

@@ -1,11 +1,15 @@
from __future__ import unicode_literals
from netaddr import IPNetwork
from django.core.exceptions import ValidationError
from django.db import models
from netaddr import AddrFormatError, IPNetwork
from .formfields import IPFormField
from . import lookups
from .lookups import (
EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals,
NetHost, NetHostContained, NetMaskLength, Regex, StartsWith,
)
def prefix_validator(prefix):
@@ -26,9 +30,7 @@ class BaseIPField(models.Field):
return value
try:
return IPNetwork(value)
except AddrFormatError as e:
raise ValidationError("Invalid IP address format: {}".format(value))
except (TypeError, ValueError) as e:
except ValueError as e:
raise ValidationError(e)
def get_prep_value(self, value):
@@ -56,18 +58,17 @@ class IPNetworkField(BaseIPField):
return 'cidr'
IPNetworkField.register_lookup(lookups.IExact)
IPNetworkField.register_lookup(lookups.EndsWith)
IPNetworkField.register_lookup(lookups.IEndsWith)
IPNetworkField.register_lookup(lookups.StartsWith)
IPNetworkField.register_lookup(lookups.IStartsWith)
IPNetworkField.register_lookup(lookups.Regex)
IPNetworkField.register_lookup(lookups.IRegex)
IPNetworkField.register_lookup(lookups.NetContained)
IPNetworkField.register_lookup(lookups.NetContainedOrEqual)
IPNetworkField.register_lookup(lookups.NetContains)
IPNetworkField.register_lookup(lookups.NetContainsOrEquals)
IPNetworkField.register_lookup(lookups.NetMaskLength)
IPNetworkField.register_lookup(EndsWith)
IPNetworkField.register_lookup(IEndsWith)
IPNetworkField.register_lookup(StartsWith)
IPNetworkField.register_lookup(IStartsWith)
IPNetworkField.register_lookup(Regex)
IPNetworkField.register_lookup(IRegex)
IPNetworkField.register_lookup(NetContained)
IPNetworkField.register_lookup(NetContainedOrEqual)
IPNetworkField.register_lookup(NetContains)
IPNetworkField.register_lookup(NetContainsOrEquals)
IPNetworkField.register_lookup(NetMaskLength)
class IPAddressField(BaseIPField):
@@ -80,17 +81,16 @@ class IPAddressField(BaseIPField):
return 'inet'
IPAddressField.register_lookup(lookups.IExact)
IPAddressField.register_lookup(lookups.EndsWith)
IPAddressField.register_lookup(lookups.IEndsWith)
IPAddressField.register_lookup(lookups.StartsWith)
IPAddressField.register_lookup(lookups.IStartsWith)
IPAddressField.register_lookup(lookups.Regex)
IPAddressField.register_lookup(lookups.IRegex)
IPAddressField.register_lookup(lookups.NetContained)
IPAddressField.register_lookup(lookups.NetContainedOrEqual)
IPAddressField.register_lookup(lookups.NetContains)
IPAddressField.register_lookup(lookups.NetContainsOrEquals)
IPAddressField.register_lookup(lookups.NetHost)
IPAddressField.register_lookup(lookups.NetHostContained)
IPAddressField.register_lookup(lookups.NetMaskLength)
IPAddressField.register_lookup(EndsWith)
IPAddressField.register_lookup(IEndsWith)
IPAddressField.register_lookup(StartsWith)
IPAddressField.register_lookup(IStartsWith)
IPAddressField.register_lookup(Regex)
IPAddressField.register_lookup(IRegex)
IPAddressField.register_lookup(NetContained)
IPAddressField.register_lookup(NetContainedOrEqual)
IPAddressField.register_lookup(NetContains)
IPAddressField.register_lookup(NetContainsOrEquals)
IPAddressField.register_lookup(NetHost)
IPAddressField.register_lookup(NetHostContained)
IPAddressField.register_lookup(NetMaskLength)

View File

@@ -1,18 +1,20 @@
from __future__ import unicode_literals
import django_filters
from django.core.exceptions import ValidationError
from django.db.models import Q
import netaddr
from netaddr import IPNetwork
from netaddr.core import AddrFormatError
from django.db.models import Q
from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NumericInFilter
from virtualization.models import VirtualMachine
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from .models import (
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -26,14 +28,11 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
def search(self, queryset, name, value):
if not value.strip():
@@ -73,9 +72,6 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='RIR (slug)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = Aggregate
@@ -86,7 +82,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset
qs_filter = Q(description__icontains=value)
try:
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
prefix = str(IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except (AddrFormatError, ValueError):
pass
@@ -106,17 +102,9 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search',
label='Search',
)
within = django_filters.CharFilter(
method='search_within',
label='Within prefix',
)
within_include = django_filters.CharFilter(
method='search_within_include',
label='Within and including prefix',
)
contains = django_filters.CharFilter(
method='search_contains',
label='Prefixes which contain this prefix or IP',
parent = django_filters.CharFilter(
method='search_by_parent',
label='Parent prefix',
)
mask_length = django_filters.NumberFilter(
method='filter_mask_length',
@@ -127,7 +115,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF',
)
vrf = django_filters.ModelMultipleChoiceFilter(
name='vrf__rd',
name='vrf',
queryset=VRF.objects.all(),
to_field_name='rd',
label='VRF (RD)',
@@ -137,7 +125,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -147,7 +135,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
@@ -165,17 +153,13 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='role__slug',
name='role',
queryset=Role.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=PREFIX_STATUS_CHOICES,
null_value=None
)
tag = django_filters.CharFilter(
name='tags__slug',
choices=PREFIX_STATUS_CHOICES
)
class Meta:
@@ -187,46 +171,22 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset
qs_filter = Q(description__icontains=value)
try:
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
prefix = str(IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except (AddrFormatError, ValueError):
pass
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(netaddr.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()
if not value:
return queryset
try:
query = str(netaddr.IPNetwork(value).cidr)
query = str(IPNetwork(value).cidr)
return queryset.filter(prefix__net_contained_or_equal=query)
except (AddrFormatError, ValueError):
return queryset.none()
def search_contains(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
# Searching by prefix
if '/' in value:
return queryset.filter(prefix__net_contains_or_equals=str(netaddr.IPNetwork(value).cidr))
# Searching by IP address
else:
return queryset.filter(prefix__net_contains=str(netaddr.IPAddress(value)))
except (AddrFormatError, ValueError):
return queryset.none()
def filter_mask_length(self, queryset, name, value):
if not value:
return queryset
@@ -243,10 +203,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search_by_parent',
label='Parent prefix',
)
address = django_filters.CharFilter(
method='filter_address',
label='Address',
)
mask_length = django_filters.NumberFilter(
method='filter_mask_length',
label='Mask length',
@@ -256,7 +212,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF',
)
vrf = django_filters.ModelMultipleChoiceFilter(
name='vrf__rd',
name='vrf',
queryset=VRF.objects.all(),
to_field_name='rd',
label='VRF (RD)',
@@ -266,21 +222,22 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
device = django_filters.CharFilter(
method='filter_device',
name='name',
label='Device',
)
device_id = django_filters.NumberFilter(
method='filter_device',
name='pk',
device_id = django_filters.ModelMultipleChoiceFilter(
name='interface__device',
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelMultipleChoiceFilter(
name='interface__device__name',
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
)
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
name='interface__virtual_machine',
queryset=VirtualMachine.objects.all(),
@@ -297,15 +254,11 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Interface (ID)',
)
status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES,
null_value=None
choices=IPADDRESS_STATUS_CHOICES
)
role = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_ROLE_CHOICES
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = IPAddress
@@ -325,35 +278,16 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value:
return queryset
try:
query = str(netaddr.IPNetwork(value.strip()).cidr)
query = str(IPNetwork(value.strip()).cidr)
return queryset.filter(address__net_host_contained=query)
except (AddrFormatError, ValueError):
return queryset.none()
def filter_address(self, queryset, name, value):
if not value.strip():
return queryset
try:
# Match address and subnet mask
if '/' in value:
return queryset.filter(address=value)
return queryset.filter(address__net_host=value)
except ValidationError:
return queryset.none()
def filter_mask_length(self, queryset, name, value):
if not value:
return queryset
return queryset.filter(address__net_mask_length=value)
def filter_device(self, queryset, name, value):
try:
device = Device.objects.select_related('device_type').get(**{name: value})
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
return queryset.filter(interface_id__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()
class VLANGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
@@ -361,7 +295,7 @@ class VLANGroupFilter(django_filters.FilterSet):
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
@@ -383,7 +317,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
@@ -393,7 +327,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
name='group__slug',
name='group',
queryset=VLANGroup.objects.all(),
to_field_name='slug',
label='Group',
@@ -403,7 +337,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant__slug',
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -413,17 +347,13 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='role__slug',
name='role',
queryset=Role.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=VLAN_STATUS_CHOICES,
null_value=None
)
tag = django_filters.CharFilter(
name='tags__slug',
choices=VLAN_STATUS_CHOICES
)
class Meta:
@@ -442,10 +372,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class ServiceFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
@@ -466,16 +392,7 @@ class ServiceFilter(django_filters.FilterSet):
to_field_name='name',
label='Virtual machine (name)',
)
tag = django_filters.CharFilter(
name='tags__slug',
)
class Meta:
model = Service
fields = ['name', 'protocol', 'port']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
return queryset.filter(qs_filter)

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