mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-14 15:52:18 -06:00
Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93fccd5985 | ||
|
|
e55acf8c63 | ||
|
|
c19e358eef | ||
|
|
efe7b46021 | ||
|
|
ededd3f464 | ||
|
|
ac2aa7ea89 | ||
|
|
04c9ebd46d | ||
|
|
c3c3b80cd9 | ||
|
|
29c4394e64 | ||
|
|
76b9a1c3af | ||
|
|
6184eb6664 | ||
|
|
e413012cbb | ||
|
|
ea2e734ba8 | ||
|
|
4ee63f4ff8 | ||
|
|
946a1b751b | ||
|
|
4ab40c4489 | ||
|
|
7944ee6419 | ||
|
|
72690bfd0a | ||
|
|
0f0d0c150a | ||
|
|
3b9ac3b986 | ||
|
|
79b1bbb9e1 | ||
|
|
533b4082d8 | ||
|
|
81d955ab7d | ||
|
|
57373c9d6f | ||
|
|
bc9158a74f | ||
|
|
9f3647cd53 | ||
|
|
d294e916a4 | ||
|
|
249faffe42 | ||
|
|
3327954a34 | ||
|
|
3b76377cac | ||
|
|
9889e120bd | ||
|
|
00e0fb5798 | ||
|
|
1fd189f9b1 | ||
|
|
b73f980eb2 | ||
|
|
65ea2af4b7 | ||
|
|
8a9c6ce37a | ||
|
|
1bbe7f95d6 | ||
|
|
d09ede8d1f | ||
|
|
bcb9ab7116 | ||
|
|
75c3e62ca8 | ||
|
|
38aee33df0 | ||
|
|
2daffdf087 | ||
|
|
03d71f9764 | ||
|
|
fa906c74c0 | ||
|
|
d933d034e0 | ||
|
|
397943b222 | ||
|
|
6b41794e12 | ||
|
|
b6e5bafd65 | ||
|
|
e6c06b39e8 | ||
|
|
a25534f3de | ||
|
|
aa6c840c45 | ||
|
|
aee9314bbf | ||
|
|
3bb10bca1b | ||
|
|
1f9e4dc707 | ||
|
|
76efea87ff | ||
|
|
483ad256a8 | ||
|
|
618566abe8 | ||
|
|
1413f5d89e | ||
|
|
4cc84aed5a | ||
|
|
300e67388b | ||
|
|
2981ead41b | ||
|
|
e4960873f3 | ||
|
|
2abee211a2 | ||
|
|
65b008a493 | ||
|
|
2236d2f941 | ||
|
|
41b2b7dbf6 | ||
|
|
27c21237ff | ||
|
|
faa12abc70 | ||
|
|
7ca4c816c0 | ||
|
|
6f68628377 | ||
|
|
82a98f0e8f | ||
|
|
1939db1574 | ||
|
|
06af05708a | ||
|
|
fa2ccc1c18 | ||
|
|
b790d7d50f | ||
|
|
af5dba2e0d | ||
|
|
589cbeb559 | ||
|
|
8dbeec8b00 | ||
|
|
8f4980044a | ||
|
|
cc4470ade7 | ||
|
|
3b4c8fa49c | ||
|
|
b4d68382ce | ||
|
|
4be5c33905 | ||
|
|
88b022d742 | ||
|
|
e5b19a9374 | ||
|
|
bd6e68fe6c | ||
|
|
8e2a69af56 | ||
|
|
450c51604c | ||
|
|
d47bf4ab6b | ||
|
|
d241cce502 | ||
|
|
c466dc5999 | ||
|
|
b62cd32428 | ||
|
|
b9223dda1a | ||
|
|
b9c09b2fc2 | ||
|
|
deda796e42 | ||
|
|
55ab720695 | ||
|
|
275223ec53 | ||
|
|
f44b20bbda | ||
|
|
c96d03cc4b | ||
|
|
8cb38de7d5 | ||
|
|
d2c3fea5b9 | ||
|
|
8ee083f7c1 | ||
|
|
9a9e3c1479 | ||
|
|
48b8602c3f | ||
|
|
e1fc78bc44 | ||
|
|
65fb10059a | ||
|
|
2e8211399d | ||
|
|
6fe40ef223 | ||
|
|
3f94295d7e | ||
|
|
5c59677c57 | ||
|
|
0bd2aa9289 | ||
|
|
19d7caf1da | ||
|
|
b8d7dd170e | ||
|
|
c643e3a74f | ||
|
|
2d690ca38a | ||
|
|
c65b9fcb0b | ||
|
|
4f6f032ca2 | ||
|
|
50d20650b4 | ||
|
|
783341017f | ||
|
|
c9dc6d04ef | ||
|
|
82ad479037 | ||
|
|
0d46a65a36 | ||
|
|
7a50cd2320 | ||
|
|
5ba5e8def9 | ||
|
|
4f347d3428 | ||
|
|
d6c2fe2385 | ||
|
|
cb4643d810 | ||
|
|
d201dad535 | ||
|
|
32d8cf451a | ||
|
|
46da9866e3 | ||
|
|
534e6ac19e | ||
|
|
518af1b95c | ||
|
|
4f95ce4984 | ||
|
|
da10b34738 | ||
|
|
a9ab0a012f | ||
|
|
45a8ee7325 | ||
|
|
23451fe974 | ||
|
|
5def0e91d7 | ||
|
|
f301af5ecd | ||
|
|
dd62caf2f0 | ||
|
|
4a00971d44 | ||
|
|
bf44e512ff | ||
|
|
4e64e1ea95 | ||
|
|
026403ed38 | ||
|
|
f6bd1f0c48 | ||
|
|
66489438b9 | ||
|
|
e5a6a4f05e | ||
|
|
9e4aa9c056 | ||
|
|
4ce40891f0 | ||
|
|
46b1ac23af | ||
|
|
a5f6e64849 | ||
|
|
b9db1ac7f7 | ||
|
|
124c2acad7 | ||
|
|
2691590aa1 | ||
|
|
51cc0d5083 | ||
|
|
9c32943d73 | ||
|
|
4483ba55dd | ||
|
|
f20e0edb35 | ||
|
|
aed2180142 | ||
|
|
4913d25d18 | ||
|
|
9e181c20c7 | ||
|
|
404d934736 | ||
|
|
024c7da15b | ||
|
|
d3a5b82d93 | ||
|
|
1e3a03c463 | ||
|
|
bafbc052e2 | ||
|
|
9421ec040c | ||
|
|
07fc2e5502 | ||
|
|
9098001bcb | ||
|
|
300aff71bb | ||
|
|
d9bf199e75 | ||
|
|
6f1ed9fc16 | ||
|
|
96b496ffa8 | ||
|
|
f1b6f0cfee | ||
|
|
e19ce043d6 | ||
|
|
35a2671525 | ||
|
|
03542b400d | ||
|
|
73d24532c9 | ||
|
|
b60f964835 | ||
|
|
8e7e02a622 | ||
|
|
2c23ca33a2 | ||
|
|
69affb7a6e | ||
|
|
6a6cf14a38 | ||
|
|
e1da3b8f10 | ||
|
|
da50cd0f03 | ||
|
|
d80ffd2308 | ||
|
|
18846cf40a | ||
|
|
e81a2094df | ||
|
|
173a4cde8b | ||
|
|
d9867423de |
@@ -1,37 +1,52 @@
|
|||||||
# Contributing to NetBox
|
## Getting Help
|
||||||
|
|
||||||
Thank you for your interest in contributing to NetBox! This document contains some quick pointers on reporting bugs and
|
If you encounter any issues installing or using NetBox, try one of the following resources to get assistance. Please
|
||||||
requesting new features.
|
**do not** open an issue on GitHub except to report bugs or request features.
|
||||||
|
|
||||||
## Reporting Issues
|
### Freenode IRC
|
||||||
|
|
||||||
* First, ensure that you've installed the latest stable version of NetBox. If you're running an older version, it's
|
Join the #netbox channel on [Freenode IRC](https://freenode.net/). You can connect to Freenode at irc.freenode.net using
|
||||||
possible that the bug has already been fixed.
|
an IRC client, or you can use their [webchat client](https://webchat.freenode.net/).
|
||||||
|
|
||||||
* Check the [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has already been
|
### Reddit
|
||||||
reported. If you think you may be experiencing a reported issue, please add a quick comment to it with a "+1" and a
|
|
||||||
quick description of how it's affecting your installation.
|
|
||||||
|
|
||||||
* If you're unsure whether the behavior you're seeing is expected, you can join #netbox on irc.freenode.net and ask
|
We have established [/r/netbox](https://www.reddit.com/r/netbox) on Reddit for NetBox issues and general discussion.
|
||||||
before going through the trouble of submitting an issue report.
|
Reddit registration is free and does not require providing an email address (although it is encouraged).
|
||||||
|
|
||||||
* When submitting an issue, please be as descriptive as possible. Be sure to describe:
|
## 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.
|
||||||
|
|
||||||
|
* 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 IRC or Reddit.
|
||||||
|
**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:
|
||||||
|
|
||||||
* The environment in which NetBox is running
|
* The environment in which NetBox is running
|
||||||
* The exact steps that can be taken to reproduce the issue (if applicable)
|
* The exact steps that can be taken to reproduce the issue (if applicable)
|
||||||
* Any error messages returned
|
* Any error messages returned
|
||||||
|
* Screenshots (if applicable)
|
||||||
|
|
||||||
* Keep in mind that we prioritize bugs based on their severity and how much work is required to resolve them. It may
|
* 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. If it's been longer than a week with no updates, please ping us on
|
take some time for someone to address your issue.
|
||||||
IRC.
|
|
||||||
|
|
||||||
## Feature Requests
|
## Feature Requests
|
||||||
|
|
||||||
* First, check the [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you'd like to see
|
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
|
||||||
has already been requested (and possibly rejected). If it is, be sure to comment with a "+1" and any additional
|
requesting is already listed. (Be sure to search closed issues as well, since some feature requests are rejected.) If
|
||||||
justification you have for the feature.
|
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.
|
||||||
|
|
||||||
* While discussion of new features is welcome, it's important to limit the scope of NetBox's feature set to avoid
|
* 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:
|
feature creep. For example, the following features would be firmly out of scope for NetBox:
|
||||||
|
|
||||||
* Ticket management
|
* Ticket management
|
||||||
@@ -39,14 +54,18 @@ feature creep. For example, the following features would be firmly out of scope
|
|||||||
* Acting as a DNS server
|
* Acting as a DNS server
|
||||||
* Acting as an authentication server
|
* Acting as an authentication server
|
||||||
|
|
||||||
* If you're not sure whether the feature you want is a good fit for NetBox, please ask in #netbox on irc.freenode.net.
|
* Before filing a new feature request, propose it on IRC or Reddit first. Feedback you receive there will help validate
|
||||||
Even if it's not quite right for NetBox, we may be able to point you to a tool better suited for the job.
|
and shape the proposed feature before filing a formal issue.
|
||||||
|
|
||||||
* When submitting a feature request, be sure to include the following:
|
* 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 chances are of being implemented. Overly broad feature
|
||||||
|
requests will be closed.
|
||||||
|
|
||||||
* A brief description of the functionality
|
* 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 use case for the feature; who would use it and what value it would add to NetBox
|
||||||
* A rough description of any changes necessary to the database schema (if applicable)
|
* A rough description of any changes necessary to the database schema
|
||||||
* Any third-party libraries or other resources which would be involved
|
* Any third-party libraries or other resources which would be involved
|
||||||
|
|
||||||
## Submitting Pull Requests
|
## Submitting Pull Requests
|
||||||
@@ -55,9 +74,8 @@ Even if it's not quite right for NetBox, we may be able to point you to a tool b
|
|||||||
before beginning work. This will help prevent wasting time on something that might we might not be able to implement.
|
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 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 branch `develop`, rather than branch `master`.
|
* When submitting a pull request, please be sure to work off of the `develop` branch, rather than `master`. In NetBox,
|
||||||
In NetBox, the `develop` branch is used for ongoing development, while `master` is used for tagging new
|
the `develop` branch is used for ongoing development, while `master` is used for tagging new stable releases.
|
||||||
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):
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ NetBox is an IP address management (IPAM) and data center infrastructure managem
|
|||||||
|
|
||||||
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/latest/).
|
||||||
|
|
||||||
Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net**!
|
Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net**!
|
||||||
|
|
||||||
### Build Status
|
### Build Status
|
||||||
@@ -23,6 +25,6 @@ Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net*
|
|||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
Please see docs/getting-started.md for instructions on installing NetBox.
|
Please see [the documentation](http://netbox.readthedocs.io/en/latest/) for instructions on installing NetBox.
|
||||||
|
|
||||||
To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.
|
To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ NetBox's local configuration is held in `netbox/netbox/configuration.py`. An exa
|
|||||||
|
|
||||||
## ALLOWED_HOSTS
|
## ALLOWED_HOSTS
|
||||||
|
|
||||||
This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
This is a list of valid fully-qualified domain names (FQDNs) that is used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different (e.g. when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server). NetBox will not permit access to the server via any other hostnames (or IPs). The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts `HTTP POST` to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/1.9/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, has `USE_X_FORWARDED_HOST = True` (in `netbox/netbox/settings.py`) which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/1.9/ref/settings/#allowed-hosts)).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ ADMINS = [
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## BANNER_TOP
|
||||||
|
|
||||||
|
## BANNER_BOTTOM
|
||||||
|
|
||||||
|
Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. To replicate the content of the top banner in the bottom banner, set:
|
||||||
|
|
||||||
|
```
|
||||||
|
BANNER_TOP = 'Your banner text'
|
||||||
|
BANNER_BOTTOM = BANNER_TOP
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## DEBUG
|
## DEBUG
|
||||||
|
|
||||||
Default: False
|
Default: False
|
||||||
@@ -34,9 +47,17 @@ In order to send email, NetBox needs an email server configured. The following i
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# ENFORCE_GLOBAL_UNIQUE
|
||||||
|
|
||||||
|
Default: False
|
||||||
|
|
||||||
|
Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table (all prefixes and IP addresses not assigned to a VRF), set `ENFORCE_GLOBAL_UNIQUE` to True.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## LOGIN_REQUIRED
|
## LOGIN_REQUIRED
|
||||||
|
|
||||||
Default: False,
|
Default: False
|
||||||
|
|
||||||
Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||||
|
|
||||||
@@ -66,6 +87,14 @@ Determine how many objects to display per page within each list of objects.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## PREFER_IPV4
|
||||||
|
|
||||||
|
Default: False
|
||||||
|
|
||||||
|
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## TIME_ZONE
|
## TIME_ZONE
|
||||||
|
|
||||||
Default: UTC
|
Default: UTC
|
||||||
|
|||||||
22
docs/data-model/tenancy.md
Normal file
22
docs/data-model/tenancy.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.
|
||||||
|
|
||||||
|
# Tenants
|
||||||
|
|
||||||
|
A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance.
|
||||||
|
|
||||||
|
The following objects can be assigned to tenants:
|
||||||
|
|
||||||
|
* Sites
|
||||||
|
* Racks
|
||||||
|
* Devices
|
||||||
|
* VRFs
|
||||||
|
* Prefixes
|
||||||
|
* IP addresses
|
||||||
|
* VLANs
|
||||||
|
* Circuits
|
||||||
|
|
||||||
|
If a prefix or IP address is not assigned to a tenant, it will appear to inherit the tenant to which its parent VRF is assigned, if any.
|
||||||
|
|
||||||
|
### Tenant Groups
|
||||||
|
|
||||||
|
Tenants can be grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional.
|
||||||
@@ -50,4 +50,4 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
|||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
|
|
||||||
See the [getting started](getting-started.md) guide for help with getting NetBox up and running quickly.
|
See the [installation guide](installation/postgresql.md) for help getting NetBox up and running quickly.
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ Generate a random secret key of at least 50 alphanumeric characters. This key mu
|
|||||||
|
|
||||||
You may use the script located at `netbox/generate_secret_key.py` to generate a suitable key.
|
You may use the script located at `netbox/generate_secret_key.py` to generate a suitable key.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
In the case of a highly available installation with multiple web servers, `SECRET_KEY` must be identical among all servers in order to maintain a persistent user session state.
|
||||||
|
|
||||||
# Run Database Migrations
|
# Run Database Migrations
|
||||||
|
|
||||||
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
|
||||||
@@ -160,6 +163,18 @@ Are you sure you want to do this?
|
|||||||
Type 'yes' to continue, or 'no' to cancel: yes
|
Type 'yes' to continue, or 'no' to cancel: yes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Load Initial Data (Optional)
|
||||||
|
|
||||||
|
NetBox ships with some initial data to help you get started: RIR definitions, common devices roles, etc. You can delete any seed data that you don't want to keep.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
|
||||||
|
|
||||||
|
```
|
||||||
|
# ./manage.py loaddata initial_data
|
||||||
|
Installed 43 object(s) from 4 fixture(s)
|
||||||
|
```
|
||||||
|
|
||||||
# Test the Application
|
# Test the Application
|
||||||
|
|
||||||
At this point, NetBox should be able to run. We can verify this by starting a development instance:
|
At this point, NetBox should be able to run. We can verify this by starting a development instance:
|
||||||
|
|||||||
@@ -4,42 +4,40 @@ As with the initial installation, you can upgrade NetBox by either downloading t
|
|||||||
|
|
||||||
## Option A: Download a Release
|
## Option A: Download a Release
|
||||||
|
|
||||||
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`. For this guide we are using 1.0.4 as the old version and 1.0.7 as the new version.
|
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`.
|
||||||
|
|
||||||
|
Download and extract the latest version:
|
||||||
|
|
||||||
Download & extract latest version:
|
|
||||||
```
|
```
|
||||||
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
||||||
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||||
# cd /opt/
|
# cd /opt/
|
||||||
# ln -sf netbox-1.0.7/ netbox
|
# ln -sf netbox-X.Y.Z/ netbox
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy the 'configuration.py' you created when first installing to the new version:
|
Copy the 'configuration.py' you created when first installing to the new version:
|
||||||
|
|
||||||
```
|
```
|
||||||
# cp /opt/netbox-1.0.4/configuration.py /opt/netbox/configuration.py
|
# cp /opt/netbox-X.Y.Z/configuration.py /opt/netbox/configuration.py
|
||||||
|
```
|
||||||
|
|
||||||
|
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
||||||
|
|
||||||
|
```
|
||||||
|
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Option B: Clone the Git Repository (latest master release)
|
## Option B: Clone the Git Repository (latest master release)
|
||||||
|
|
||||||
For this guide, we'll use `/opt/netbox`.
|
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
||||||
|
|
||||||
Check that your git branch is up to date & is set to master:
|
|
||||||
```
|
```
|
||||||
# cd /opt/netbox
|
# cd /opt/netbox
|
||||||
# git status
|
|
||||||
```
|
|
||||||
|
|
||||||
If not on branch master, set it and verify status:
|
|
||||||
```
|
|
||||||
# git checkout master
|
# git checkout master
|
||||||
|
# git pull origin master
|
||||||
# git status
|
# git status
|
||||||
```
|
```
|
||||||
|
|
||||||
Pull down the set branch from git status above:
|
|
||||||
```
|
|
||||||
# git pull
|
|
||||||
```
|
|
||||||
|
|
||||||
# Run the Upgrade Script
|
# Run the Upgrade Script
|
||||||
|
|
||||||
Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured).
|
Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured).
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pages:
|
|||||||
- 'DCIM': 'data-model/dcim.md'
|
- 'DCIM': 'data-model/dcim.md'
|
||||||
- 'IPAM': 'data-model/ipam.md'
|
- 'IPAM': 'data-model/ipam.md'
|
||||||
- 'Secrets': 'data-model/secrets.md'
|
- 'Secrets': 'data-model/secrets.md'
|
||||||
|
- 'Tenancy': 'data-model/tenancy.md'
|
||||||
- 'Extras': 'data-model/extras.md'
|
- 'Extras': 'data-model/extras.md'
|
||||||
- 'API Integration': 'api-integration.md'
|
- 'API Integration': 'api-integration.md'
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ class CircuitTypeAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Circuit)
|
@admin.register(Circuit)
|
||||||
class CircuitAdmin(admin.ModelAdmin):
|
class CircuitAdmin(admin.ModelAdmin):
|
||||||
list_display = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id']
|
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate',
|
||||||
list_filter = ['provider']
|
'xconnect_id']
|
||||||
|
list_filter = ['provider', 'type', 'tenant']
|
||||||
exclude = ['interface']
|
exclude = ['interface']
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(CircuitAdmin, self).get_queryset(request)
|
qs = super(CircuitAdmin, self).get_queryset(request)
|
||||||
return qs.select_related('provider', 'type', 'site')
|
return qs.select_related('provider', 'type', 'tenant', 'site')
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from circuits.models import Provider, CircuitType, Circuit
|
from circuits.models import Provider, CircuitType, Circuit
|
||||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||||
|
from tenancy.api.serializers import TenantNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -45,13 +46,14 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
|
|||||||
class CircuitSerializer(serializers.ModelSerializer):
|
class CircuitSerializer(serializers.ModelSerializer):
|
||||||
provider = ProviderNestedSerializer()
|
provider = ProviderNestedSerializer()
|
||||||
type = CircuitTypeNestedSerializer()
|
type = CircuitTypeNestedSerializer()
|
||||||
|
tenant = TenantNestedSerializer()
|
||||||
site = SiteNestedSerializer()
|
site = SiteNestedSerializer()
|
||||||
interface = InterfaceNestedSerializer()
|
interface = InterfaceNestedSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['id', 'cid', 'provider', 'type', 'site', 'interface', 'install_date', 'port_speed', 'commit_rate',
|
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
|
||||||
'xconnect_id', 'comments']
|
'commit_rate', 'xconnect_id', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class CircuitNestedSerializer(CircuitSerializer):
|
class CircuitNestedSerializer(CircuitSerializer):
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class CircuitListView(generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
List circuits (filterable)
|
List circuits (filterable)
|
||||||
"""
|
"""
|
||||||
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
|
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
filter_class = CircuitFilter
|
filter_class = CircuitFilter
|
||||||
|
|
||||||
@@ -51,5 +51,5 @@ class CircuitDetailView(generics.RetrieveAPIView):
|
|||||||
"""
|
"""
|
||||||
Retrieve a single circuit
|
Retrieve a single circuit
|
||||||
"""
|
"""
|
||||||
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
|
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
|
|||||||
@@ -1,9 +1,41 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
|
from tenancy.models import Tenant
|
||||||
from .models import Provider, Circuit, CircuitType
|
from .models import Provider, Circuit, CircuitType
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.MethodFilter(
|
||||||
|
action='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='circuits__site',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
label='Site',
|
||||||
|
)
|
||||||
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='circuits__site',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Site (slug)',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Provider
|
||||||
|
fields = ['q', 'name', 'account', 'asn']
|
||||||
|
|
||||||
|
def search(self, queryset, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(account__icontains=value) |
|
||||||
|
Q(comments__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilter(django_filters.FilterSet):
|
class CircuitFilter(django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.MethodFilter(
|
||||||
action='search',
|
action='search',
|
||||||
@@ -31,6 +63,17 @@ class CircuitFilter(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Circuit type (slug)',
|
label='Circuit type (slug)',
|
||||||
)
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
label='Tenant (ID)',
|
||||||
|
)
|
||||||
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@@ -48,5 +91,9 @@ class CircuitFilter(django_filters.FilterSet):
|
|||||||
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
|
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
value = value.strip()
|
return queryset.filter(
|
||||||
return queryset.filter(cid__icontains=value)
|
Q(cid__icontains=value) |
|
||||||
|
Q(xconnect_id__icontains=value) |
|
||||||
|
Q(pp_info__icontains=value) |
|
||||||
|
Q(comments__icontains=value)
|
||||||
|
)
|
||||||
|
|||||||
26
netbox/circuits/fixtures/initial_data.json
Normal file
26
netbox/circuits/fixtures/initial_data.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "circuits.circuittype",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "Internet",
|
||||||
|
"slug": "internet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "circuits.circuittype",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "Private WAN",
|
||||||
|
"slug": "private-wan"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "circuits.circuittype",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "Out-of-Band",
|
||||||
|
"slug": "out-of-band"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -2,9 +2,10 @@ from django import forms
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||||
|
from tenancy.forms import bulkedit_tenant_choices
|
||||||
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, ConfirmationForm, CSVDataField, Livesearch, SmallTextarea,
|
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
|
||||||
SlugField,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import Circuit, CircuitType, Provider
|
from .models import Circuit, CircuitType, Provider
|
||||||
@@ -55,8 +56,14 @@ class ProviderBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
|
|
||||||
class ProviderBulkDeleteForm(ConfirmationForm):
|
def provider_site_choices():
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
site_choices = Site.objects.all()
|
||||||
|
return [(s.slug, s.name) for s in site_choices]
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderFilterForm(forms.Form, BootstrapMixin):
|
||||||
|
site = forms.MultipleChoiceField(required=False, choices=provider_site_choices,
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -71,10 +78,6 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=CircuitType.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
@@ -98,7 +101,7 @@ class CircuitForm(forms.ModelForm, BootstrapMixin):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = [
|
fields = [
|
||||||
'cid', 'type', 'provider', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
|
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
|
||||||
'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
|
'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
@@ -159,13 +162,15 @@ class CircuitFromCSVForm(forms.ModelForm):
|
|||||||
error_messages={'invalid_choice': 'Provider not found.'})
|
error_messages={'invalid_choice': 'Provider not found.'})
|
||||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
|
site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
error_messages={'invalid_choice': 'Site not found.'})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id',
|
fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate',
|
||||||
'pp_info']
|
'xconnect_id', 'pp_info']
|
||||||
|
|
||||||
|
|
||||||
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
||||||
@@ -176,33 +181,37 @@ class CircuitBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||||
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
||||||
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
||||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def circuit_type_choices():
|
def circuit_type_choices():
|
||||||
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||||
return [(t.slug, '{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
|
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
|
||||||
|
|
||||||
|
|
||||||
def circuit_provider_choices():
|
def circuit_provider_choices():
|
||||||
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
|
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
|
||||||
return [(p.slug, '{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
|
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
|
||||||
|
|
||||||
|
|
||||||
|
def circuit_tenant_choices():
|
||||||
|
tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
|
||||||
|
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices]
|
||||||
|
|
||||||
|
|
||||||
def circuit_site_choices():
|
def circuit_site_choices():
|
||||||
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
|
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
|
||||||
return [(s.slug, '{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
|
return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilterForm(forms.Form, BootstrapMixin):
|
class CircuitFilterForm(forms.Form, BootstrapMixin):
|
||||||
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
|
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
|
||||||
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
|
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
|
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-13 19:24
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import dcim.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0002_auto_20160622_1821'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='asn',
|
||||||
|
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
|
||||||
|
),
|
||||||
|
]
|
||||||
22
netbox/circuits/migrations/0004_circuit_add_tenant.py
Normal file
22
netbox/circuits/migrations/0004_circuit_add_tenant.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-07-26 21:59
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tenancy', '0001_initial'),
|
||||||
|
('circuits', '0003_provider_32bit_asn_support'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='tenant',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from dcim.fields import ASNField
|
||||||
from dcim.models import Site, Interface
|
from dcim.models import Site, Interface
|
||||||
|
from tenancy.models import Tenant
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
|
|
||||||
|
|
||||||
@@ -12,7 +14,7 @@ class Provider(CreatedUpdatedModel):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(max_length=50, unique=True)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
|
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
||||||
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
|
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
|
||||||
portal_url = models.URLField(blank=True, verbose_name='Portal')
|
portal_url = models.URLField(blank=True, verbose_name='Portal')
|
||||||
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
|
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
|
||||||
@@ -65,6 +67,7 @@ class Circuit(CreatedUpdatedModel):
|
|||||||
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
|
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
|
||||||
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
||||||
type = models.ForeignKey('CircuitType', 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)
|
||||||
site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
|
site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
|
||||||
interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
|
interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
|
||||||
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
|
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
|
||||||
@@ -79,7 +82,7 @@ class Circuit(CreatedUpdatedModel):
|
|||||||
unique_together = ['provider', 'cid']
|
unique_together = ['provider', 'cid']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "{0} {1}".format(self.provider, self.cid)
|
return u'{} {}'.format(self.provider, self.cid)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('circuits:circuit', args=[self.pk])
|
return reverse('circuits:circuit', args=[self.pk])
|
||||||
@@ -89,6 +92,7 @@ class Circuit(CreatedUpdatedModel):
|
|||||||
self.cid,
|
self.cid,
|
||||||
self.provider.name,
|
self.provider.name,
|
||||||
self.type.name,
|
self.type.name,
|
||||||
|
self.tenant.name if self.tenant else '',
|
||||||
self.site.name,
|
self.site.name,
|
||||||
self.install_date.isoformat() if self.install_date else '',
|
self.install_date.isoformat() if self.install_date else '',
|
||||||
str(self.port_speed),
|
str(self.port_speed),
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
|
|||||||
from .models import Circuit, CircuitType, Provider
|
from .models import Circuit, CircuitType, Provider
|
||||||
|
|
||||||
|
|
||||||
CIRCUITTYPE_EDIT_LINK = """
|
CIRCUITTYPE_ACTIONS = """
|
||||||
{% if perms.circuit.change_circuittype %}
|
{% if perms.circuit.change_circuittype %}
|
||||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}">Edit</a>
|
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -21,11 +21,12 @@ class ProviderTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
|
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
|
||||||
asn = tables.Column(verbose_name='ASN')
|
asn = tables.Column(verbose_name='ASN')
|
||||||
|
account = tables.Column(verbose_name='Account')
|
||||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ('pk', 'name', 'asn', 'circuit_count')
|
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -37,11 +38,12 @@ class CircuitTypeTable(BaseTable):
|
|||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
circuit_count = tables.Column(verbose_name='Circuits')
|
circuit_count = tables.Column(verbose_name='Circuits')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
edit = tables.TemplateColumn(template_code=CIRCUITTYPE_EDIT_LINK, verbose_name='')
|
actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name='')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = ('pk', 'name', 'circuit_count', 'slug', 'edit')
|
fields = ('pk', 'name', 'circuit_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -53,10 +55,13 @@ class CircuitTable(BaseTable):
|
|||||||
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
|
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
|
||||||
type = tables.Column(verbose_name='Type')
|
type = tables.Column(verbose_name='Type')
|
||||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
||||||
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
port_speed_human = tables.Column(verbose_name='Port Speed')
|
port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'),
|
||||||
commit_rate_human = tables.Column(verbose_name='Commit Rate')
|
verbose_name='Port Speed')
|
||||||
|
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
|
||||||
|
verbose_name='Commit Rate')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ('pk', 'cid', 'type', 'provider', 'site', 'port_speed_human', 'commit_rate_human')
|
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate')
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
@@ -16,6 +17,8 @@ from .models import Circuit, CircuitType, Provider
|
|||||||
|
|
||||||
class ProviderListView(ObjectListView):
|
class ProviderListView(ObjectListView):
|
||||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||||
|
filter = filters.ProviderFilter
|
||||||
|
filter_form = forms.ProviderFilterForm
|
||||||
table = tables.ProviderTable
|
table = tables.ProviderTable
|
||||||
edit_permissions = ['circuits.change_provider', 'circuits.delete_provider']
|
edit_permissions = ['circuits.change_provider', 'circuits.delete_provider']
|
||||||
template_name = 'circuits/provider_list.html'
|
template_name = 'circuits/provider_list.html'
|
||||||
@@ -25,10 +28,12 @@ def provider(request, slug):
|
|||||||
|
|
||||||
provider = get_object_or_404(Provider, slug=slug)
|
provider = get_object_or_404(Provider, slug=slug)
|
||||||
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
|
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
|
||||||
|
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||||
|
|
||||||
return render(request, 'circuits/provider.html', {
|
return render(request, 'circuits/provider.html', {
|
||||||
'provider': provider,
|
'provider': provider,
|
||||||
'circuits': circuits,
|
'circuits': circuits,
|
||||||
|
'show_graphs': show_graphs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -74,7 +79,6 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_provider'
|
permission_required = 'circuits.delete_provider'
|
||||||
cls = Provider
|
cls = Provider
|
||||||
form = forms.ProviderBulkDeleteForm
|
|
||||||
default_redirect_url = 'circuits:provider_list'
|
default_redirect_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -100,7 +104,6 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_circuittype'
|
permission_required = 'circuits.delete_circuittype'
|
||||||
cls = CircuitType
|
cls = CircuitType
|
||||||
form = forms.CircuitTypeBulkDeleteForm
|
|
||||||
default_redirect_url = 'circuits:circuittype_list'
|
default_redirect_url = 'circuits:circuittype_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -109,7 +112,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitListView(ObjectListView):
|
class CircuitListView(ObjectListView):
|
||||||
queryset = Circuit.objects.select_related('provider', 'type', 'site')
|
queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site')
|
||||||
filter = filters.CircuitFilter
|
filter = filters.CircuitFilter
|
||||||
filter_form = forms.CircuitFilterForm
|
filter_form = forms.CircuitFilterForm
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
@@ -159,6 +162,10 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form):
|
||||||
|
|
||||||
fields_to_update = {}
|
fields_to_update = {}
|
||||||
|
if form.cleaned_data['tenant'] == 0:
|
||||||
|
fields_to_update['tenant'] = None
|
||||||
|
elif form.cleaned_data['tenant']:
|
||||||
|
fields_to_update['tenant'] = form.cleaned_data['tenant']
|
||||||
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
|
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
|
||||||
if form.cleaned_data[field]:
|
if form.cleaned_data[field]:
|
||||||
fields_to_update[field] = form.cleaned_data[field]
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
@@ -169,5 +176,4 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_circuit'
|
permission_required = 'circuits.delete_circuit'
|
||||||
cls = Circuit
|
cls = Circuit
|
||||||
form = forms.CircuitBulkDeleteForm
|
|
||||||
default_redirect_url = 'circuits:circuit_list'
|
default_redirect_url = 'circuits:circuit_list'
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
default_app_config = 'dcim.apps.IPAMConfig'
|
default_app_config = 'dcim.apps.DCIMConfig'
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ class DeviceTypeAdmin(admin.ModelAdmin):
|
|||||||
InterfaceTemplateAdmin,
|
InterfaceTemplateAdmin,
|
||||||
DeviceBayTemplateAdmin,
|
DeviceBayTemplateAdmin,
|
||||||
]
|
]
|
||||||
list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports',
|
list_display = ['model', 'manufacturer', 'slug', 'part_number', 'u_height', 'console_ports', 'console_server_ports',
|
||||||
'power_outlets', 'interfaces', 'device_bays']
|
'power_ports', 'power_outlets', 'interfaces', 'device_bays']
|
||||||
list_filter = ['manufacturer']
|
list_filter = ['manufacturer']
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
@@ -180,4 +180,4 @@ class DeviceAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(DeviceAdmin, self).get_queryset(request)
|
qs = super(DeviceAdmin, self).get_queryset(request)
|
||||||
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip', 'rack')
|
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from dcim.models import (
|
|||||||
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
||||||
)
|
)
|
||||||
|
from tenancy.api.serializers import TenantNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -13,10 +14,11 @@ from dcim.models import (
|
|||||||
#
|
#
|
||||||
|
|
||||||
class SiteSerializer(serializers.ModelSerializer):
|
class SiteSerializer(serializers.ModelSerializer):
|
||||||
|
tenant = TenantNestedSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['id', 'name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
|
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
|
||||||
'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@ class RackGroupSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'site']
|
fields = ['id', 'name', 'slug', 'site']
|
||||||
|
|
||||||
|
|
||||||
class RackGroupNestedSerializer(SiteSerializer):
|
class RackGroupNestedSerializer(RackGroupSerializer):
|
||||||
|
|
||||||
class Meta(SiteSerializer.Meta):
|
class Meta(SiteSerializer.Meta):
|
||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
@@ -52,10 +54,11 @@ class RackGroupNestedSerializer(SiteSerializer):
|
|||||||
class RackSerializer(serializers.ModelSerializer):
|
class RackSerializer(serializers.ModelSerializer):
|
||||||
site = SiteNestedSerializer()
|
site = SiteNestedSerializer()
|
||||||
group = RackGroupNestedSerializer()
|
group = RackGroupNestedSerializer()
|
||||||
|
tenant = TenantNestedSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments']
|
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class RackNestedSerializer(RackSerializer):
|
class RackNestedSerializer(RackSerializer):
|
||||||
@@ -69,8 +72,8 @@ class RackDetailSerializer(RackSerializer):
|
|||||||
rear_units = serializers.SerializerMethodField()
|
rear_units = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta(RackSerializer.Meta):
|
class Meta(RackSerializer.Meta):
|
||||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments', 'front_units',
|
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments',
|
||||||
'rear_units']
|
'front_units', 'rear_units']
|
||||||
|
|
||||||
def get_front_units(self, obj):
|
def get_front_units(self, obj):
|
||||||
units = obj.get_rack_units(face=RACK_FACE_FRONT)
|
units = obj.get_rack_units(face=RACK_FACE_FRONT)
|
||||||
@@ -111,8 +114,8 @@ class DeviceTypeSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'is_network_device']
|
'is_console_server', 'is_pdu', 'is_network_device']
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeNestedSerializer(DeviceTypeSerializer):
|
class DeviceTypeNestedSerializer(DeviceTypeSerializer):
|
||||||
@@ -164,9 +167,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer):
|
|||||||
interface_templates = InterfaceTemplateNestedSerializer(many=True, read_only=True)
|
interface_templates = InterfaceTemplateNestedSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta(DeviceTypeSerializer.Meta):
|
class Meta(DeviceTypeSerializer.Meta):
|
||||||
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'is_network_device', 'console_port_templates', 'cs_port_templates', 'power_port_templates',
|
'is_console_server', 'is_pdu', 'is_network_device', 'console_port_templates', 'cs_port_templates',
|
||||||
'power_outlet_templates', 'interface_templates']
|
'power_port_templates', 'power_outlet_templates', 'interface_templates']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -218,6 +221,7 @@ class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
|
|||||||
class DeviceSerializer(serializers.ModelSerializer):
|
class DeviceSerializer(serializers.ModelSerializer):
|
||||||
device_type = DeviceTypeNestedSerializer()
|
device_type = DeviceTypeNestedSerializer()
|
||||||
device_role = DeviceRoleNestedSerializer()
|
device_role = DeviceRoleNestedSerializer()
|
||||||
|
tenant = TenantNestedSerializer()
|
||||||
platform = PlatformNestedSerializer()
|
platform = PlatformNestedSerializer()
|
||||||
rack = RackNestedSerializer()
|
rack = RackNestedSerializer()
|
||||||
primary_ip = DeviceIPAddressNestedSerializer()
|
primary_ip = DeviceIPAddressNestedSerializer()
|
||||||
@@ -227,8 +231,8 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
|
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'rack',
|
||||||
'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
|
'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
|
||||||
|
|
||||||
def get_parent_device(self, obj):
|
def get_parent_device(self, obj):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ urlpatterns = [
|
|||||||
url(r'^interfaces/(?P<pk>\d+)/$', InterfaceDetailView.as_view(), name='interface_detail'),
|
url(r'^interfaces/(?P<pk>\d+)/$', InterfaceDetailView.as_view(), name='interface_detail'),
|
||||||
url(r'^interfaces/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE},
|
url(r'^interfaces/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE},
|
||||||
name='interface_graphs'),
|
name='interface_graphs'),
|
||||||
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection'),
|
url(r'^interface-connections/$', InterfaceConnectionListView.as_view(), name='interfaceconnection_list'),
|
||||||
|
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection_detail'),
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
|
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class SiteListView(generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
List all sites
|
List all sites
|
||||||
"""
|
"""
|
||||||
queryset = Site.objects.all()
|
queryset = Site.objects.select_related('tenant')
|
||||||
serializer_class = serializers.SiteSerializer
|
serializer_class = serializers.SiteSerializer
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class SiteDetailView(generics.RetrieveAPIView):
|
|||||||
"""
|
"""
|
||||||
Retrieve a single site
|
Retrieve a single site
|
||||||
"""
|
"""
|
||||||
queryset = Site.objects.all()
|
queryset = Site.objects.select_related('tenant')
|
||||||
serializer_class = serializers.SiteSerializer
|
serializer_class = serializers.SiteSerializer
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class RackGroupListView(generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
List all rack groups
|
List all rack groups
|
||||||
"""
|
"""
|
||||||
queryset = RackGroup.objects.all()
|
queryset = RackGroup.objects.select_related('site')
|
||||||
serializer_class = serializers.RackGroupSerializer
|
serializer_class = serializers.RackGroupSerializer
|
||||||
filter_class = filters.RackGroupFilter
|
filter_class = filters.RackGroupFilter
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class RackGroupDetailView(generics.RetrieveAPIView):
|
|||||||
"""
|
"""
|
||||||
Retrieve a single rack group
|
Retrieve a single rack group
|
||||||
"""
|
"""
|
||||||
queryset = RackGroup.objects.all()
|
queryset = RackGroup.objects.select_related('site')
|
||||||
serializer_class = serializers.RackGroupSerializer
|
serializer_class = serializers.RackGroupSerializer
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ class RackListView(generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
List racks (filterable)
|
List racks (filterable)
|
||||||
"""
|
"""
|
||||||
queryset = Rack.objects.select_related('site')
|
queryset = Rack.objects.select_related('site', 'group', 'tenant')
|
||||||
serializer_class = serializers.RackSerializer
|
serializer_class = serializers.RackSerializer
|
||||||
filter_class = filters.RackFilter
|
filter_class = filters.RackFilter
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class RackDetailView(generics.RetrieveAPIView):
|
|||||||
"""
|
"""
|
||||||
Retrieve a single rack
|
Retrieve a single rack
|
||||||
"""
|
"""
|
||||||
queryset = Rack.objects.select_related('site')
|
queryset = Rack.objects.select_related('site', 'group', 'tenant')
|
||||||
serializer_class = serializers.RackDetailSerializer
|
serializer_class = serializers.RackDetailSerializer
|
||||||
|
|
||||||
|
|
||||||
@@ -193,8 +193,9 @@ class DeviceListView(generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
List devices (filterable)
|
List devices (filterable)
|
||||||
"""
|
"""
|
||||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
|
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
|
||||||
.prefetch_related('primary_ip4__nat_outside', 'primary_ip6__nat_outside')
|
'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside',
|
||||||
|
'primary_ip6__nat_outside')
|
||||||
serializer_class = serializers.DeviceSerializer
|
serializer_class = serializers.DeviceSerializer
|
||||||
filter_class = filters.DeviceFilter
|
filter_class = filters.DeviceFilter
|
||||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
|
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
|
||||||
@@ -204,7 +205,8 @@ class DeviceDetailView(generics.RetrieveAPIView):
|
|||||||
"""
|
"""
|
||||||
Retrieve a single device
|
Retrieve a single device
|
||||||
"""
|
"""
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
|
||||||
|
'rack__site', 'parent_bay')
|
||||||
serializer_class = serializers.DeviceSerializer
|
serializer_class = serializers.DeviceSerializer
|
||||||
|
|
||||||
|
|
||||||
@@ -326,6 +328,14 @@ class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
queryset = InterfaceConnection.objects.all()
|
queryset = InterfaceConnection.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceConnectionListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
Retrieve a list of all interface connections
|
||||||
|
"""
|
||||||
|
serializer_class = serializers.InterfaceConnectionSerializer
|
||||||
|
queryset = InterfaceConnection.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device bays
|
# Device bays
|
||||||
#
|
#
|
||||||
@@ -411,53 +421,36 @@ class RelatedConnectionsView(APIView):
|
|||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise MissingFilterException(detail='Must specify search parameters (peer-device and peer-interface).')
|
raise MissingFilterException(detail='Must specify search parameters "peer-device" and "peer-interface".')
|
||||||
|
|
||||||
# Initialize response skeleton
|
# Initialize response skeleton
|
||||||
response = dict()
|
response = {
|
||||||
response['device'] = serializers.DeviceSerializer(device).data
|
'device': serializers.DeviceSerializer(device).data,
|
||||||
response['console-ports'] = []
|
'console-ports': [],
|
||||||
response['power-ports'] = []
|
'power-ports': [],
|
||||||
response['interfaces'] = []
|
'interfaces': [],
|
||||||
|
}
|
||||||
|
|
||||||
# Build console connections
|
# Console connections
|
||||||
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
|
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
|
||||||
for cp in console_ports:
|
for cp in console_ports:
|
||||||
cp_info = dict()
|
data = serializers.ConsolePortSerializer(instance=cp).data
|
||||||
cp_info['name'] = cp.name
|
del(data['device'])
|
||||||
if cp.cs_port:
|
response['console-ports'].append(data)
|
||||||
cp_info['console-server'] = cp.cs_port.device.name
|
|
||||||
cp_info['port'] = cp.cs_port.name
|
|
||||||
else:
|
|
||||||
cp_info['console-server'] = None
|
|
||||||
cp_info['port'] = None
|
|
||||||
response['console-ports'].append(cp_info)
|
|
||||||
|
|
||||||
# Build power connections
|
# Power connections
|
||||||
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
|
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
|
||||||
for pp in power_ports:
|
for pp in power_ports:
|
||||||
pp_info = dict()
|
data = serializers.PowerPortSerializer(instance=pp).data
|
||||||
pp_info['name'] = pp.name
|
del(data['device'])
|
||||||
if pp.power_outlet:
|
response['power-ports'].append(data)
|
||||||
pp_info['pdu'] = pp.power_outlet.device.name
|
|
||||||
pp_info['outlet'] = pp.power_outlet.name
|
|
||||||
else:
|
|
||||||
pp_info['pdu'] = None
|
|
||||||
pp_info['outlet'] = None
|
|
||||||
response['power-ports'].append(pp_info)
|
|
||||||
|
|
||||||
# Built interface connections
|
# Interface connections
|
||||||
interfaces = Interface.objects.filter(device=device)
|
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
|
||||||
|
'circuit')
|
||||||
for iface in interfaces:
|
for iface in interfaces:
|
||||||
iface_info = dict()
|
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
||||||
iface_info['name'] = iface.name
|
del(data['device'])
|
||||||
peer_interface = iface.get_connected_interface()
|
response['interfaces'].append(data)
|
||||||
if peer_interface:
|
|
||||||
iface_info['device'] = peer_interface.device.name
|
|
||||||
iface_info['interface'] = peer_interface.name
|
|
||||||
else:
|
|
||||||
iface_info['device'] = None
|
|
||||||
iface_info['interface'] = None
|
|
||||||
response['interfaces'].append(iface_info)
|
|
||||||
|
|
||||||
return Response(response)
|
return Response(response)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class IPAMConfig(AppConfig):
|
class DCIMConfig(AppConfig):
|
||||||
name = "dcim"
|
name = "dcim"
|
||||||
verbose_name = "DCIM"
|
verbose_name = "DCIM"
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
from netaddr import EUI, mac_unix_expanded
|
from netaddr import EUI, mac_unix_expanded
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from .formfields import MACAddressFormField
|
from .formfields import MACAddressFormField
|
||||||
|
|
||||||
|
|
||||||
|
class ASNField(models.BigIntegerField):
|
||||||
|
description = "32-bit ASN field"
|
||||||
|
default_validators = [
|
||||||
|
MinValueValidator(1),
|
||||||
|
MaxValueValidator(4294967295),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
||||||
word_fmt = '%.2X'
|
word_fmt = '%.2X'
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from .models import (
|
|||||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
||||||
Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
|
Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
|
||||||
)
|
)
|
||||||
|
from tenancy.models import Tenant
|
||||||
|
|
||||||
|
|
||||||
class SiteFilter(django_filters.FilterSet):
|
class SiteFilter(django_filters.FilterSet):
|
||||||
@@ -13,17 +14,27 @@ class SiteFilter(django_filters.FilterSet):
|
|||||||
action='search',
|
action='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
label='Tenant (ID)',
|
||||||
|
)
|
||||||
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['q', 'name', 'facility', 'asn']
|
fields = ['q', 'name', 'facility', 'asn']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
value = value.strip()
|
|
||||||
qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
|
qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
|
||||||
Q(shipping_address__icontains=value)
|
Q(shipping_address__icontains=value) | Q(comments__icontains=value)
|
||||||
try:
|
try:
|
||||||
qs_filter |= Q(asn=int(value))
|
qs_filter |= Q(asn=int(value.strip()))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
@@ -74,16 +85,27 @@ class RackFilter(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Group',
|
label='Group',
|
||||||
)
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
label='Tenant (ID)',
|
||||||
|
)
|
||||||
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['q', 'site_id', 'site', 'u_height']
|
fields = ['q', 'site_id', 'site', 'u_height']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
value = value.strip()
|
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(facility_id__icontains=value)
|
Q(facility_id__icontains=value) |
|
||||||
|
Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -102,7 +124,7 @@ class DeviceTypeFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['manufacturer_id', 'manufacturer', 'model', 'u_height', 'is_console_server', 'is_pdu',
|
fields = ['manufacturer_id', 'manufacturer', 'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu',
|
||||||
'is_network_device']
|
'is_network_device']
|
||||||
|
|
||||||
|
|
||||||
@@ -122,6 +144,11 @@ class DeviceFilter(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site name (slug)',
|
label='Site name (slug)',
|
||||||
)
|
)
|
||||||
|
rack_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='rack__group',
|
||||||
|
queryset=RackGroup.objects.all(),
|
||||||
|
label='Rack group (ID)',
|
||||||
|
)
|
||||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack',
|
name='rack',
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
@@ -138,6 +165,17 @@ class DeviceFilter(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
label='Tenant (ID)',
|
||||||
|
)
|
||||||
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
|
)
|
||||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device_type',
|
name='device_type',
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
@@ -195,11 +233,11 @@ class DeviceFilter(django_filters.FilterSet):
|
|||||||
'is_network_device']
|
'is_network_device']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
value = value.strip()
|
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(serial__icontains=value) |
|
Q(serial__icontains=value) |
|
||||||
Q(modules__serial__icontains=value)
|
Q(modules__serial__icontains=value) |
|
||||||
|
Q(comments__icontains=value)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
201
netbox/dcim/fixtures/initial_data.json
Normal file
201
netbox/dcim/fixtures/initial_data.json
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "dcim.devicerole",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "Console Server",
|
||||||
|
"slug": "console-server",
|
||||||
|
"color": "teal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.devicerole",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "Core Switch",
|
||||||
|
"slug": "core-switch",
|
||||||
|
"color": "blue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.devicerole",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "Distribution Switch",
|
||||||
|
"slug": "distribution-switch",
|
||||||
|
"color": "blue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.devicerole",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "Access Switch",
|
||||||
|
"slug": "access-switch",
|
||||||
|
"color": "blue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.devicerole",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"name": "Management Switch",
|
||||||
|
"slug": "management-switch",
|
||||||
|
"color": "orange"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.devicerole",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"name": "Firewall",
|
||||||
|
"slug": "firewall",
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.devicerole",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"name": "Router",
|
||||||
|
"slug": "router",
|
||||||
|
"color": "purple"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.devicerole",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"name": "Server",
|
||||||
|
"slug": "server",
|
||||||
|
"color": "medium_gray"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.devicerole",
|
||||||
|
"pk": 9,
|
||||||
|
"fields": {
|
||||||
|
"name": "PDU",
|
||||||
|
"slug": "pdu",
|
||||||
|
"color": "dark_gray"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.manufacturer",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "APC",
|
||||||
|
"slug": "apc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.manufacturer",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "Cisco",
|
||||||
|
"slug": "cisco"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.manufacturer",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "Dell",
|
||||||
|
"slug": "dell"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.manufacturer",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "HP",
|
||||||
|
"slug": "hp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.manufacturer",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"name": "Juniper",
|
||||||
|
"slug": "juniper"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.manufacturer",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"name": "Arista",
|
||||||
|
"slug": "arista"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.manufacturer",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"name": "Opengear",
|
||||||
|
"slug": "opengear"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.manufacturer",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"name": "Super Micro",
|
||||||
|
"slug": "super-micro"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.platform",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "Cisco IOS",
|
||||||
|
"slug": "cisco-ios",
|
||||||
|
"rpc_client": "cisco-ios"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.platform",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "Cisco NX-OS",
|
||||||
|
"slug": "cisco-nx-os",
|
||||||
|
"rpc_client": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.platform",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "Juniper Junos",
|
||||||
|
"slug": "juniper-junos",
|
||||||
|
"rpc_client": "juniper-junos"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.platform",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "Arista EOS",
|
||||||
|
"slug": "arista-eos",
|
||||||
|
"rpc_client": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.platform",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"name": "Linux",
|
||||||
|
"slug": "linux",
|
||||||
|
"rpc_client": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "dcim.platform",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"name": "Opengear",
|
||||||
|
"slug": "opengear",
|
||||||
|
"rpc_client": "opengear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -4,8 +4,10 @@ from django import forms
|
|||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
|
from tenancy.forms import bulkedit_tenant_choices
|
||||||
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, ConfirmationForm, CSVDataField, ExpandableNameField,
|
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
|
||||||
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,6 +40,15 @@ def get_device_by_name_or_pk(name):
|
|||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def bulkedit_platform_choices():
|
||||||
|
choices = [
|
||||||
|
(None, '---------'),
|
||||||
|
(0, 'None'),
|
||||||
|
]
|
||||||
|
choices += [(p.pk, p.name) for p in Platform.objects.all()]
|
||||||
|
return choices
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
@@ -48,7 +59,7 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
|
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
|
||||||
widgets = {
|
widgets = {
|
||||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||||
'shipping_address': SmallTextarea(attrs={'rows': 3}),
|
'shipping_address': SmallTextarea(attrs={'rows': 3}),
|
||||||
@@ -63,16 +74,33 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
|
|
||||||
class SiteFromCSVForm(forms.ModelForm):
|
class SiteFromCSVForm(forms.ModelForm):
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['name', 'slug', 'facility', 'asn']
|
fields = ['name', 'slug', 'tenant', 'facility', 'asn']
|
||||||
|
|
||||||
|
|
||||||
class SiteImportForm(BulkImportForm, BootstrapMixin):
|
class SiteImportForm(BulkImportForm, BootstrapMixin):
|
||||||
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
|
class SiteBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
|
|
||||||
|
|
||||||
|
def site_tenant_choices():
|
||||||
|
tenant_choices = Tenant.objects.annotate(site_count=Count('sites'))
|
||||||
|
return [(t.slug, u'{} ({})'.format(t.name, t.site_count)) for t in tenant_choices]
|
||||||
|
|
||||||
|
|
||||||
|
class SiteFilterForm(forms.Form, BootstrapMixin):
|
||||||
|
tenant = forms.MultipleChoiceField(required=False, choices=site_tenant_choices,
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack groups
|
# Rack groups
|
||||||
#
|
#
|
||||||
@@ -85,13 +113,9 @@ class RackGroupForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['site', 'name', 'slug']
|
fields = ['site', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class RackGroupBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=RackGroup.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def rackgroup_site_choices():
|
def rackgroup_site_choices():
|
||||||
site_choices = Site.objects.annotate(rack_count=Count('rack_groups'))
|
site_choices = Site.objects.annotate(rack_count=Count('rack_groups'))
|
||||||
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||||
|
|
||||||
|
|
||||||
class RackGroupFilterForm(forms.Form, BootstrapMixin):
|
class RackGroupFilterForm(forms.Form, BootstrapMixin):
|
||||||
@@ -111,7 +135,7 @@ class RackForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['site', 'group', 'name', 'facility_id', 'u_height', 'comments']
|
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'u_height', 'comments']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which the rack exists",
|
'site': "The site at which the rack exists",
|
||||||
'name': "Organizational rack name",
|
'name': "Organizational rack name",
|
||||||
@@ -139,10 +163,12 @@ class RackFromCSVForm(forms.ModelForm):
|
|||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
error_messages={'invalid_choice': 'Site not found.'})
|
||||||
group_name = forms.CharField(required=False)
|
group_name = forms.CharField(required=False)
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['site', 'group_name', 'name', 'facility_id', 'u_height']
|
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'u_height']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
@@ -165,28 +191,32 @@ class RackBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
|
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
|
||||||
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
u_height = forms.IntegerField(required=False, label='Height (U)')
|
u_height = forms.IntegerField(required=False, label='Height (U)')
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
|
|
||||||
class RackBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def rack_site_choices():
|
def rack_site_choices():
|
||||||
site_choices = Site.objects.annotate(rack_count=Count('racks'))
|
site_choices = Site.objects.annotate(rack_count=Count('racks'))
|
||||||
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||||
|
|
||||||
|
|
||||||
def rack_group_choices():
|
def rack_group_choices():
|
||||||
group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
||||||
return [(g.pk, '{} ({})'.format(g, g.rack_count)) for g in group_choices]
|
return [(g.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
|
||||||
|
|
||||||
|
|
||||||
|
def rack_tenant_choices():
|
||||||
|
tenant_choices = Tenant.objects.annotate(rack_count=Count('racks'))
|
||||||
|
return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in tenant_choices]
|
||||||
|
|
||||||
|
|
||||||
class RackFilterForm(forms.Form, BootstrapMixin):
|
class RackFilterForm(forms.Form, BootstrapMixin):
|
||||||
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
|
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices,
|
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
|
||||||
|
|
||||||
@@ -202,10 +232,6 @@ class ManufacturerForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Manufacturer.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
@@ -215,8 +241,8 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||||
'is_network_device', 'subdevice_role']
|
'is_pdu', 'is_network_device', 'subdevice_role']
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
@@ -225,13 +251,9 @@ class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
u_height = forms.IntegerField(min_value=1, required=False)
|
u_height = forms.IntegerField(min_value=1, required=False)
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def devicetype_manufacturer_choices():
|
def devicetype_manufacturer_choices():
|
||||||
manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
||||||
return [(m.slug, '{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
|
return [(m.slug, u'{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
||||||
@@ -303,10 +325,6 @@ class DeviceRoleForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name', 'slug', 'color']
|
fields = ['name', 'slug', 'color']
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceRole.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Platforms
|
# Platforms
|
||||||
#
|
#
|
||||||
@@ -319,10 +337,6 @@ class PlatformForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class PlatformBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Platform.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
@@ -348,7 +362,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
|
fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
|
||||||
'platform', 'primary_ip4', 'primary_ip6', 'comments']
|
'platform', 'primary_ip4', 'primary_ip6', 'comments']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'device_role': "The function this device serves",
|
'device_role': "The function this device serves",
|
||||||
@@ -373,10 +387,10 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
for family in [4, 6]:
|
for family in [4, 6]:
|
||||||
ip_choices = []
|
ip_choices = []
|
||||||
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
|
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
|
||||||
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
ip_choices += [(ip.id, u'{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||||
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
|
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
|
||||||
.select_related('nat_inside__interface')
|
.select_related('nat_inside__interface')
|
||||||
ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||||
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
|
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -396,8 +410,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
self.fields['rack'].choices = []
|
self.fields['rack'].choices = []
|
||||||
|
|
||||||
# Rack position
|
# Rack position
|
||||||
try:
|
|
||||||
pk = self.instance.pk if self.instance.pk else None
|
pk = self.instance.pk if self.instance.pk else None
|
||||||
|
try:
|
||||||
if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
|
if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
|
||||||
position_choices = Rack.objects.get(pk=self.data['rack'])\
|
position_choices = Rack.objects.get(pk=self.data['rack'])\
|
||||||
.get_rack_units(face=self.data.get('face'), exclude=pk)
|
.get_rack_units(face=self.data.get('face'), exclude=pk)
|
||||||
@@ -425,32 +439,31 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
else:
|
else:
|
||||||
self.fields['device_type'].choices = []
|
self.fields['device_type'].choices = []
|
||||||
|
|
||||||
|
# Disable rack assignment if this is a child device installed in a parent device
|
||||||
|
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
||||||
|
self.fields['site'].disabled = True
|
||||||
|
self.fields['rack'].disabled = True
|
||||||
|
|
||||||
class DeviceFromCSVForm(forms.ModelForm):
|
|
||||||
|
class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid device role.'})
|
error_messages={'invalid_choice': 'Invalid device role.'})
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid manufacturer.'})
|
error_messages={'invalid_choice': 'Invalid manufacturer.'})
|
||||||
model_name = forms.CharField()
|
model_name = forms.CharField()
|
||||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
|
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid platform.'})
|
error_messages={'invalid_choice': 'Invalid platform.'})
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
|
||||||
'invalid_choice': 'Invalid site name.',
|
|
||||||
})
|
|
||||||
rack_name = forms.CharField()
|
|
||||||
face = forms.CharField(required=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
fields = []
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
|
|
||||||
'position', 'face']
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
manufacturer = self.cleaned_data.get('manufacturer')
|
manufacturer = self.cleaned_data.get('manufacturer')
|
||||||
model_name = self.cleaned_data.get('model_name')
|
model_name = self.cleaned_data.get('model_name')
|
||||||
site = self.cleaned_data.get('site')
|
|
||||||
rack_name = self.cleaned_data.get('rack_name')
|
|
||||||
|
|
||||||
# Validate device type
|
# Validate device type
|
||||||
if manufacturer and model_name:
|
if manufacturer and model_name:
|
||||||
@@ -459,6 +472,25 @@ class DeviceFromCSVForm(forms.ModelForm):
|
|||||||
except DeviceType.DoesNotExist:
|
except DeviceType.DoesNotExist:
|
||||||
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
|
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||||
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||||
|
'invalid_choice': 'Invalid site name.',
|
||||||
|
})
|
||||||
|
rack_name = forms.CharField()
|
||||||
|
face = forms.CharField(required=False)
|
||||||
|
|
||||||
|
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||||
|
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'site',
|
||||||
|
'rack_name', 'position', 'face']
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
super(DeviceFromCSVForm, self).clean()
|
||||||
|
|
||||||
|
site = self.cleaned_data.get('site')
|
||||||
|
rack_name = self.cleaned_data.get('rack_name')
|
||||||
|
|
||||||
# Validate rack
|
# Validate rack
|
||||||
if site and rack_name:
|
if site and rack_name:
|
||||||
try:
|
try:
|
||||||
@@ -468,7 +500,8 @@ class DeviceFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean_face(self):
|
def clean_face(self):
|
||||||
face = self.cleaned_data['face']
|
face = self.cleaned_data['face']
|
||||||
if face:
|
if not face:
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
return {
|
return {
|
||||||
'front': 0,
|
'front': 0,
|
||||||
@@ -476,52 +509,95 @@ class DeviceFromCSVForm(forms.ModelForm):
|
|||||||
}[face.lower()]
|
}[face.lower()]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
|
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
|
||||||
return face
|
|
||||||
|
|
||||||
|
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||||
|
parent = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Parent device not found.'})
|
||||||
|
device_bay_name = forms.CharField(required=False)
|
||||||
|
|
||||||
|
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||||
|
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
|
||||||
|
'device_bay_name']
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
super(ChildDeviceFromCSVForm, self).clean()
|
||||||
|
|
||||||
|
parent = self.cleaned_data.get('parent')
|
||||||
|
device_bay_name = self.cleaned_data.get('device_bay_name')
|
||||||
|
|
||||||
|
# Validate device bay
|
||||||
|
if parent and device_bay_name:
|
||||||
|
try:
|
||||||
|
device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
|
||||||
|
if device_bay.installed_device:
|
||||||
|
self.add_error('device_bay_name',
|
||||||
|
"Device bay ({} {}) is already occupied".format(parent, device_bay_name))
|
||||||
|
else:
|
||||||
|
self.instance.parent_bay = device_bay
|
||||||
|
except DeviceBay.DoesNotExist:
|
||||||
|
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
|
||||||
|
|
||||||
|
|
||||||
class DeviceImportForm(BulkImportForm, BootstrapMixin):
|
class DeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||||
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
|
class ChildDeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||||
|
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditForm(forms.Form, BootstrapMixin):
|
class DeviceBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
||||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
||||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, label='Platform')
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
platform_delete = forms.BooleanField(required=False, label='Set platform to "none"')
|
platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False,
|
||||||
|
label='Platform')
|
||||||
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
|
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
|
||||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def device_site_choices():
|
def device_site_choices():
|
||||||
site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
|
site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
|
||||||
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
return [(s.slug, u'{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
||||||
|
|
||||||
|
|
||||||
|
def device_rack_group_choices():
|
||||||
|
group_choices = RackGroup.objects.select_related('site').annotate(device_count=Count('racks__devices'))
|
||||||
|
return [(g.pk, u'{} ({})'.format(g, g.device_count)) for g in group_choices]
|
||||||
|
|
||||||
|
|
||||||
def device_role_choices():
|
def device_role_choices():
|
||||||
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
|
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||||
return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
|
return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
|
||||||
|
|
||||||
|
|
||||||
|
def device_tenant_choices():
|
||||||
|
tenant_choices = Tenant.objects.annotate(device_count=Count('devices'))
|
||||||
|
return [(t.slug, u'{} ({})'.format(t.name, t.device_count)) for t in tenant_choices]
|
||||||
|
|
||||||
|
|
||||||
def device_type_choices():
|
def device_type_choices():
|
||||||
type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
|
type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
|
||||||
return [(t.pk, '{} ({})'.format(t, t.device_count)) for t in type_choices]
|
return [(t.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
|
||||||
|
|
||||||
|
|
||||||
def device_platform_choices():
|
def device_platform_choices():
|
||||||
platform_choices = Platform.objects.annotate(device_count=Count('devices'))
|
platform_choices = Platform.objects.annotate(device_count=Count('devices'))
|
||||||
return [(p.slug, '{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
|
return [(p.slug, u'{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterForm(forms.Form, BootstrapMixin):
|
class DeviceFilterForm(forms.Form, BootstrapMixin):
|
||||||
site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
|
site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
rack_group_id = forms.MultipleChoiceField(required=False, choices=device_rack_group_choices, label='Rack Group',
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
|
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
tenant = forms.MultipleChoiceField(required=False, choices=device_tenant_choices,
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
|
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
|
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
|
||||||
|
|||||||
21
netbox/dcim/migrations/0009_site_32bit_asn_support.py
Normal file
21
netbox/dcim/migrations/0009_site_32bit_asn_support.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-13 19:24
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import dcim.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0008_device_remove_primary_ip'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='site',
|
||||||
|
name='asn',
|
||||||
|
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-14 21:38
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0009_site_32bit_asn_support'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicebay',
|
||||||
|
name='installed_device',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_bay', to='dcim.Device'),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
netbox/dcim/migrations/0011_devicetype_part_number.py
Normal file
20
netbox/dcim/migrations/0011_devicetype_part_number.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-07-26 15:05
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0010_devicebay_installed_device_set_null'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='part_number',
|
||||||
|
field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
32
netbox/dcim/migrations/0012_site_rack_device_add_tenant.py
Normal file
32
netbox/dcim/migrations/0012_site_rack_device_add_tenant.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-07-26 21:59
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tenancy', '0001_initial'),
|
||||||
|
('dcim', '0011_devicetype_part_number'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='tenant',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='tenant',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='site',
|
||||||
|
name='tenant',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
netbox/dcim/migrations/0013_add_interface_form_factors.py
Normal file
25
netbox/dcim/migrations/0013_add_interface_form_factors.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-08-06 20:24
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0012_site_rack_device_add_tenant'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import MultipleObjectsReturned, ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||||
|
|
||||||
from extras.rpc import RPC_CLIENTS
|
from extras.rpc import RPC_CLIENTS
|
||||||
|
from tenancy.models import Tenant
|
||||||
from utilities.fields import NullableCharField
|
from utilities.fields import NullableCharField
|
||||||
|
from utilities.managers import NaturalOrderByManager
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
|
|
||||||
from .fields import MACAddressField
|
from .fields import ASNField, MACAddressField
|
||||||
|
|
||||||
|
|
||||||
RACK_FACE_FRONT = 0
|
RACK_FACE_FRONT = 0
|
||||||
RACK_FACE_REAR = 1
|
RACK_FACE_REAR = 1
|
||||||
@@ -53,20 +57,63 @@ DEVICE_ROLE_COLOR_CHOICES = [
|
|||||||
IFACE_FF_VIRTUAL = 0
|
IFACE_FF_VIRTUAL = 0
|
||||||
IFACE_FF_100M_COPPER = 800
|
IFACE_FF_100M_COPPER = 800
|
||||||
IFACE_FF_1GE_COPPER = 1000
|
IFACE_FF_1GE_COPPER = 1000
|
||||||
|
IFACE_FF_GBIC = 1050
|
||||||
IFACE_FF_SFP = 1100
|
IFACE_FF_SFP = 1100
|
||||||
IFACE_FF_10GE_COPPER = 1150
|
IFACE_FF_10GE_COPPER = 1150
|
||||||
IFACE_FF_SFP_PLUS = 1200
|
IFACE_FF_SFP_PLUS = 1200
|
||||||
IFACE_FF_XFP = 1300
|
IFACE_FF_XFP = 1300
|
||||||
IFACE_FF_QSFP_PLUS = 1400
|
IFACE_FF_QSFP_PLUS = 1400
|
||||||
|
IFACE_FF_CFP = 1500
|
||||||
|
IFACE_FF_QSFP28 = 1600
|
||||||
|
IFACE_FF_T1 = 4000
|
||||||
|
IFACE_FF_E1 = 4010
|
||||||
|
IFACE_FF_T3 = 4040
|
||||||
|
IFACE_FF_E3 = 4050
|
||||||
|
IFACE_FF_STACKWISE = 5000
|
||||||
|
IFACE_FF_STACKWISE_PLUS = 5050
|
||||||
IFACE_FF_CHOICES = [
|
IFACE_FF_CHOICES = [
|
||||||
|
[
|
||||||
|
'Virtual interfaces',
|
||||||
|
[
|
||||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||||
[IFACE_FF_100M_COPPER, '10/100M (100BASE-TX)'],
|
]
|
||||||
[IFACE_FF_1GE_COPPER, '1GE (1000BASE-T)'],
|
],
|
||||||
[IFACE_FF_SFP, '1GE (SFP)'],
|
[
|
||||||
[IFACE_FF_10GE_COPPER, '10GE (10GBASE-T)'],
|
'Ethernet',
|
||||||
[IFACE_FF_SFP_PLUS, '10GE (SFP+)'],
|
[
|
||||||
[IFACE_FF_XFP, '10GE (XFP)'],
|
[IFACE_FF_100M_COPPER, '100BASE-TX (10/100M)'],
|
||||||
[IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'],
|
[IFACE_FF_1GE_COPPER, '1000BASE-T (1GE)'],
|
||||||
|
[IFACE_FF_10GE_COPPER, '10GBASE-T (10GE)'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Modular',
|
||||||
|
[
|
||||||
|
[IFACE_FF_GBIC, 'GBIC (1GE)'],
|
||||||
|
[IFACE_FF_SFP, 'SFP (1GE)'],
|
||||||
|
[IFACE_FF_XFP, 'XFP (10GE)'],
|
||||||
|
[IFACE_FF_SFP_PLUS, 'SFP+ (10GE)'],
|
||||||
|
[IFACE_FF_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||||
|
[IFACE_FF_CFP, 'CFP (100GE)'],
|
||||||
|
[IFACE_FF_QSFP28, 'QSFP28 (100GE)'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Serial',
|
||||||
|
[
|
||||||
|
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
|
||||||
|
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
|
||||||
|
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
||||||
|
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Stacking',
|
||||||
|
[
|
||||||
|
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
|
||||||
|
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||||
|
]
|
||||||
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
STATUS_ACTIVE = True
|
STATUS_ACTIVE = True
|
||||||
@@ -136,6 +183,12 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
|
|||||||
}).order_by(*ordering)
|
}).order_by(*ordering)
|
||||||
|
|
||||||
|
|
||||||
|
class SiteManager(NaturalOrderByManager):
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.natural_order_by('name')
|
||||||
|
|
||||||
|
|
||||||
class Site(CreatedUpdatedModel):
|
class Site(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||||
@@ -143,12 +196,15 @@ class Site(CreatedUpdatedModel):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(max_length=50, unique=True)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
|
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT)
|
||||||
facility = models.CharField(max_length=50, blank=True)
|
facility = models.CharField(max_length=50, blank=True)
|
||||||
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
|
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
||||||
physical_address = models.CharField(max_length=200, blank=True)
|
physical_address = models.CharField(max_length=200, blank=True)
|
||||||
shipping_address = models.CharField(max_length=200, blank=True)
|
shipping_address = models.CharField(max_length=200, blank=True)
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
|
|
||||||
|
objects = SiteManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
@@ -162,6 +218,7 @@ class Site(CreatedUpdatedModel):
|
|||||||
return ','.join([
|
return ','.join([
|
||||||
self.name,
|
self.name,
|
||||||
self.slug,
|
self.slug,
|
||||||
|
self.tenant.name if self.tenant else '',
|
||||||
self.facility,
|
self.facility,
|
||||||
str(self.asn),
|
str(self.asn),
|
||||||
])
|
])
|
||||||
@@ -205,12 +262,18 @@ class RackGroup(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return '{} - {}'.format(self.site.name, self.name)
|
return u'{} - {}'.format(self.site.name, self.name)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class RackManager(NaturalOrderByManager):
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.natural_order_by('site__name', 'name')
|
||||||
|
|
||||||
|
|
||||||
class Rack(CreatedUpdatedModel):
|
class Rack(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||||
@@ -220,9 +283,12 @@ class Rack(CreatedUpdatedModel):
|
|||||||
facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
|
facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
|
||||||
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
|
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
|
||||||
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
|
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
|
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
|
||||||
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
|
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
|
|
||||||
|
objects = RackManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['site', 'name']
|
ordering = ['site', 'name']
|
||||||
unique_together = [
|
unique_together = [
|
||||||
@@ -253,6 +319,7 @@ class Rack(CreatedUpdatedModel):
|
|||||||
self.group.name if self.group else '',
|
self.group.name if self.group else '',
|
||||||
self.name,
|
self.name,
|
||||||
self.facility_id or '',
|
self.facility_id or '',
|
||||||
|
self.tenant.name if self.tenant else '',
|
||||||
str(self.u_height),
|
str(self.u_height),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -341,6 +408,15 @@ class Rack(CreatedUpdatedModel):
|
|||||||
def get_0u_devices(self):
|
def get_0u_devices(self):
|
||||||
return self.devices.filter(position=0)
|
return self.devices.filter(position=0)
|
||||||
|
|
||||||
|
def get_utilization(self):
|
||||||
|
"""
|
||||||
|
Determine the utilization rate of the rack and return it as a percentage.
|
||||||
|
"""
|
||||||
|
if self.u_consumed is None:
|
||||||
|
self.u_consumed = 0
|
||||||
|
u_available = self.u_height - self.u_consumed
|
||||||
|
return int(float(self.u_height - u_available) / self.u_height * 100)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device Types
|
# Device Types
|
||||||
@@ -381,6 +457,7 @@ class DeviceType(models.Model):
|
|||||||
manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT)
|
manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT)
|
||||||
model = models.CharField(max_length=50)
|
model = models.CharField(max_length=50)
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
|
part_number = models.CharField(max_length=50, blank=True, help_text="Discrete part number (optional)")
|
||||||
u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
|
u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
|
||||||
is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
|
is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
|
||||||
help_text="Device consumes both front and rear rack faces")
|
help_text="Device consumes both front and rear rack faces")
|
||||||
@@ -403,7 +480,7 @@ class DeviceType(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "{} {}".format(self.manufacturer, self.model)
|
return u'{} {}'.format(self.manufacturer, self.model)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:devicetype', args=[self.pk])
|
return reverse('dcim:devicetype', args=[self.pk])
|
||||||
@@ -582,6 +659,12 @@ class Platform(models.Model):
|
|||||||
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
|
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceManager(NaturalOrderByManager):
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.natural_order_by('name')
|
||||||
|
|
||||||
|
|
||||||
class Device(CreatedUpdatedModel):
|
class Device(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||||
@@ -596,6 +679,7 @@ class Device(CreatedUpdatedModel):
|
|||||||
"""
|
"""
|
||||||
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
|
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
|
||||||
device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
|
device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
|
||||||
|
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT)
|
||||||
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
|
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
|
name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
|
||||||
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
||||||
@@ -611,6 +695,8 @@ class Device(CreatedUpdatedModel):
|
|||||||
blank=True, null=True, verbose_name='Primary IPv6')
|
blank=True, null=True, verbose_name='Primary IPv6')
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
|
|
||||||
|
objects = DeviceManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
unique_together = ['rack', 'position', 'face']
|
unique_together = ['rack', 'position', 'face']
|
||||||
@@ -623,6 +709,10 @@ class Device(CreatedUpdatedModel):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
# Validate device type assignment
|
||||||
|
if not hasattr(self, 'device_type'):
|
||||||
|
raise ValidationError("Must specify device type.")
|
||||||
|
|
||||||
# Child devices cannot be assigned to a rack face/unit
|
# Child devices cannot be assigned to a rack face/unit
|
||||||
if self.device_type.is_child_device and (self.face is not None or self.position):
|
if self.device_type.is_child_device and (self.face is not None or self.position):
|
||||||
raise ValidationError("Child device types cannot be assigned a rack face or position.")
|
raise ValidationError("Child device types cannot be assigned a rack face or position.")
|
||||||
@@ -632,10 +722,7 @@ class Device(CreatedUpdatedModel):
|
|||||||
raise ValidationError("Must specify rack face with rack position.")
|
raise ValidationError("Must specify rack face with rack position.")
|
||||||
|
|
||||||
# Validate rack space
|
# Validate rack space
|
||||||
try:
|
|
||||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||||
except DeviceType.DoesNotExist:
|
|
||||||
raise ValidationError("Must specify device type.")
|
|
||||||
exclude_list = [self.pk] if self.pk else []
|
exclude_list = [self.pk] if self.pk else []
|
||||||
try:
|
try:
|
||||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||||
@@ -679,10 +766,14 @@ class Device(CreatedUpdatedModel):
|
|||||||
self.device_type.device_bay_templates.all()]
|
self.device_type.device_bay_templates.all()]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update Rack assignment for any child Devices
|
||||||
|
Device.objects.filter(parent_bay__device=self).update(rack=self.rack)
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return ','.join([
|
return ','.join([
|
||||||
self.name or '',
|
self.name or '',
|
||||||
self.device_role.name,
|
self.device_role.name,
|
||||||
|
self.tenant.name if self.tenant else '',
|
||||||
self.device_type.manufacturer.name,
|
self.device_type.manufacturer.name,
|
||||||
self.device_type.model,
|
self.device_type.model,
|
||||||
self.platform.name if self.platform else '',
|
self.platform.name if self.platform else '',
|
||||||
@@ -713,7 +804,9 @@ class Device(CreatedUpdatedModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def primary_ip(self):
|
def primary_ip(self):
|
||||||
if self.primary_ip6:
|
if settings.PREFER_IPV4 and self.primary_ip4:
|
||||||
|
return self.primary_ip4
|
||||||
|
elif self.primary_ip6:
|
||||||
return self.primary_ip6
|
return self.primary_ip6
|
||||||
elif self.primary_ip4:
|
elif self.primary_ip4:
|
||||||
return self.primary_ip4
|
return self.primary_ip4
|
||||||
@@ -915,8 +1008,8 @@ class Interface(models.Model):
|
|||||||
return connection.interface_a
|
return connection.interface_a
|
||||||
except InterfaceConnection.DoesNotExist:
|
except InterfaceConnection.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
except InterfaceConnection.MultipleObjectsReturned as e:
|
except InterfaceConnection.MultipleObjectsReturned:
|
||||||
raise e("Multiple connections found for {0} interface {1}!".format(self.device, self))
|
raise MultipleObjectsReturned("Multiple connections found for {} interface {}!".format(self.device, self))
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnection(models.Model):
|
class InterfaceConnection(models.Model):
|
||||||
@@ -950,14 +1043,15 @@ class DeviceBay(models.Model):
|
|||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='device_bays', on_delete=models.CASCADE)
|
device = models.ForeignKey('Device', related_name='device_bays', on_delete=models.CASCADE)
|
||||||
name = models.CharField(max_length=50, verbose_name='Name')
|
name = models.CharField(max_length=50, verbose_name='Name')
|
||||||
installed_device = models.OneToOneField('Device', related_name='parent_bay', blank=True, null=True)
|
installed_device = models.OneToOneField('Device', related_name='parent_bay', on_delete=models.SET_NULL, blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return '{} - {}'.format(self.device.name, self.name)
|
return u'{} - {}'.format(self.device.name, self.name)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
|||||||
@@ -16,27 +16,27 @@ DEVICE_LINK = """
|
|||||||
</a>
|
</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RACKGROUP_EDIT_LINK = """
|
RACKGROUP_ACTIONS = """
|
||||||
{% if perms.dcim.change_rackgroup %}
|
{% if perms.dcim.change_rackgroup %}
|
||||||
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}">Edit</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 %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEVICEROLE_EDIT_LINK = """
|
DEVICEROLE_ACTIONS = """
|
||||||
{% if perms.dcim.change_devicerole %}
|
{% if perms.dcim.change_devicerole %}
|
||||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}">Edit</a>
|
<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 %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MANUFACTURER_EDIT_LINK = """
|
MANUFACTURER_ACTIONS = """
|
||||||
{% if perms.dcim.change_manufacturer %}
|
{% if perms.dcim.change_manufacturer %}
|
||||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}">Edit</a>
|
<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 %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PLATFORM_EDIT_LINK = """
|
PLATFORM_ACTIONS = """
|
||||||
{% if perms.dcim.change_platform %}
|
{% if perms.dcim.change_platform %}
|
||||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}">Edit</a>
|
<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 %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -48,14 +48,21 @@ STATUS_ICON = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
UTILIZATION_GRAPH = """
|
||||||
|
{% load helpers %}
|
||||||
|
{% utilization_graph record.get_utilization %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteTable(BaseTable):
|
class SiteTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
|
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
|
||||||
facility = tables.Column(verbose_name='Facility')
|
facility = tables.Column(verbose_name='Facility')
|
||||||
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
asn = tables.Column(verbose_name='ASN')
|
asn = tables.Column(verbose_name='ASN')
|
||||||
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
||||||
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
||||||
@@ -65,8 +72,8 @@ class SiteTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Site
|
model = Site
|
||||||
fields = ('name', 'facility', 'asn', 'rack_count', 'device_count', 'prefix_count', 'vlan_count',
|
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||||
'circuit_count')
|
'vlan_count', 'circuit_count')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -79,11 +86,12 @@ class RackGroupTable(BaseTable):
|
|||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
rack_count = tables.Column(verbose_name='Racks')
|
rack_count = tables.Column(verbose_name='Racks')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
edit = tables.TemplateColumn(template_code=RACKGROUP_EDIT_LINK, verbose_name='')
|
actions = tables.TemplateColumn(template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name='')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'edit')
|
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -96,12 +104,29 @@ class RackTable(BaseTable):
|
|||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
facility_id = tables.Column(verbose_name='Facility ID')
|
facility_id = tables.Column(verbose_name='Facility ID')
|
||||||
u_height = tables.Column(verbose_name='Height (U)')
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
|
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||||
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
|
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
|
||||||
|
u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used')
|
||||||
|
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'u_height', 'devices')
|
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'u_height', 'devices', 'u_consumed',
|
||||||
|
'utilization')
|
||||||
|
|
||||||
|
|
||||||
|
class RackImportTable(BaseTable):
|
||||||
|
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
|
||||||
|
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.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
|
u_height = tables.Column(verbose_name='Height (U)')
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Rack
|
||||||
|
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -113,11 +138,12 @@ class ManufacturerTable(BaseTable):
|
|||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
devicetype_count = tables.Column(verbose_name='Device Types')
|
devicetype_count = tables.Column(verbose_name='Device Types')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
edit = tables.TemplateColumn(template_code=MANUFACTURER_EDIT_LINK, verbose_name='')
|
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name='')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = ('pk', 'name', 'devicetype_count', 'slug', 'edit')
|
fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -126,93 +152,77 @@ class ManufacturerTable(BaseTable):
|
|||||||
|
|
||||||
class DeviceTypeTable(BaseTable):
|
class DeviceTypeTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
manufacturer = tables.Column(verbose_name='Manufacturer')
|
||||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||||
|
part_number = tables.Column(verbose_name='Part Number')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ('pk', 'model', 'manufacturer', 'u_height')
|
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device type components
|
# Device type components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortTemplateTable(tables.Table):
|
class ConsolePortTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
|
||||||
class Meta:
|
class Meta(BaseTable.Meta):
|
||||||
model = ConsolePortTemplate
|
model = ConsolePortTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
show_header = False
|
||||||
attrs = {
|
|
||||||
'class': 'table table-hover',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateTable(tables.Table):
|
class ConsoleServerPortTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
|
||||||
class Meta:
|
class Meta(BaseTable.Meta):
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
show_header = False
|
||||||
attrs = {
|
|
||||||
'class': 'table table-hover',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateTable(tables.Table):
|
class PowerPortTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
|
||||||
class Meta:
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
show_header = False
|
||||||
attrs = {
|
|
||||||
'class': 'table table-hover',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateTable(tables.Table):
|
class PowerOutletTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
|
||||||
class Meta:
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
show_header = False
|
||||||
attrs = {
|
|
||||||
'class': 'table table-hover',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateTable(tables.Table):
|
class InterfaceTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
|
||||||
class Meta:
|
class Meta(BaseTable.Meta):
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name', 'form_factor')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
show_header = False
|
||||||
attrs = {
|
|
||||||
'class': 'table table-hover panel-body',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateTable(tables.Table):
|
class DeviceBayTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
|
||||||
class Meta:
|
class Meta(BaseTable.Meta):
|
||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
show_header = False
|
||||||
attrs = {
|
|
||||||
'class': 'table table-hover panel-body',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -225,11 +235,12 @@ class DeviceRoleTable(BaseTable):
|
|||||||
device_count = tables.Column(verbose_name='Devices')
|
device_count = tables.Column(verbose_name='Devices')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
color = tables.Column(verbose_name='Color')
|
color = tables.Column(verbose_name='Color')
|
||||||
edit = tables.TemplateColumn(template_code=DEVICEROLE_EDIT_LINK, verbose_name='')
|
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name='')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ('pk', 'name', 'device_count', 'slug', 'color')
|
fields = ('pk', 'name', 'device_count', 'slug', 'color', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -241,11 +252,11 @@ class PlatformTable(BaseTable):
|
|||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
device_count = tables.Column(verbose_name='Devices')
|
device_count = tables.Column(verbose_name='Devices')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
edit = tables.TemplateColumn(template_code=PLATFORM_EDIT_LINK, verbose_name='')
|
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ('pk', 'name', 'device_count', 'slug', 'edit')
|
fields = ('pk', 'name', 'device_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -256,6 +267,7 @@ class DeviceTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
||||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||||
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
||||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||||
device_role = tables.Column(verbose_name='Role')
|
device_role = tables.Column(verbose_name='Role')
|
||||||
@@ -265,11 +277,12 @@ class DeviceTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Device
|
model = Device
|
||||||
fields = ('pk', 'name', 'status', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||||
|
|
||||||
|
|
||||||
class DeviceImportTable(BaseTable):
|
class DeviceImportTable(BaseTable):
|
||||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||||
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
||||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||||
position = tables.Column(verbose_name='Position')
|
position = tables.Column(verbose_name='Position')
|
||||||
@@ -278,7 +291,7 @@ class DeviceImportTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Device
|
model = Device
|
||||||
fields = ('name', 'site', 'rack', 'position', 'device_role', 'device_type')
|
fields = ('name', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||||
empty_text = False
|
empty_text = False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class SiteTest(APITestCase):
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'slug',
|
'slug',
|
||||||
|
'tenant',
|
||||||
'facility',
|
'facility',
|
||||||
'asn',
|
'asn',
|
||||||
'physical_address',
|
'physical_address',
|
||||||
@@ -40,6 +41,7 @@ class SiteTest(APITestCase):
|
|||||||
'display_name',
|
'display_name',
|
||||||
'site',
|
'site',
|
||||||
'group',
|
'group',
|
||||||
|
'tenant',
|
||||||
'u_height',
|
'u_height',
|
||||||
'comments'
|
'comments'
|
||||||
]
|
]
|
||||||
@@ -115,6 +117,7 @@ class RackTest(APITestCase):
|
|||||||
'display_name',
|
'display_name',
|
||||||
'site',
|
'site',
|
||||||
'group',
|
'group',
|
||||||
|
'tenant',
|
||||||
'u_height',
|
'u_height',
|
||||||
'comments'
|
'comments'
|
||||||
]
|
]
|
||||||
@@ -126,6 +129,7 @@ class RackTest(APITestCase):
|
|||||||
'display_name',
|
'display_name',
|
||||||
'site',
|
'site',
|
||||||
'group',
|
'group',
|
||||||
|
'tenant',
|
||||||
'u_height',
|
'u_height',
|
||||||
'comments',
|
'comments',
|
||||||
'front_units',
|
'front_units',
|
||||||
@@ -204,6 +208,7 @@ class DeviceTypeTest(APITestCase):
|
|||||||
'manufacturer',
|
'manufacturer',
|
||||||
'model',
|
'model',
|
||||||
'slug',
|
'slug',
|
||||||
|
'part_number',
|
||||||
'u_height',
|
'u_height',
|
||||||
'is_full_depth',
|
'is_full_depth',
|
||||||
'is_console_server',
|
'is_console_server',
|
||||||
@@ -310,6 +315,7 @@ class DeviceTest(APITestCase):
|
|||||||
'display_name',
|
'display_name',
|
||||||
'device_type',
|
'device_type',
|
||||||
'device_role',
|
'device_role',
|
||||||
|
'tenant',
|
||||||
'platform',
|
'platform',
|
||||||
'serial',
|
'serial',
|
||||||
'rack',
|
'rack',
|
||||||
@@ -387,6 +393,7 @@ class DeviceTest(APITestCase):
|
|||||||
'rack_name',
|
'rack_name',
|
||||||
'serial',
|
'serial',
|
||||||
'status',
|
'status',
|
||||||
|
'tenant',
|
||||||
]
|
]
|
||||||
|
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ urlpatterns = [
|
|||||||
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
||||||
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
|
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
|
||||||
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||||
|
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
|
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
|
||||||
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
|
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-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||||
@@ -50,31 +51,29 @@ urlpatterns = [
|
|||||||
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
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+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||||
|
|
||||||
# Component templates
|
# Console port templates
|
||||||
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(),
|
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), name='devicetype_add_consoleport'),
|
||||||
name='devicetype_add_consoleport'),
|
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.component_template_delete,
|
|
||||||
{'model': ConsolePortTemplate}, name='devicetype_delete_consoleport'),
|
# Console server port templates
|
||||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(),
|
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), name='devicetype_add_consoleserverport'),
|
||||||
name='devicetype_add_consoleserverport'),
|
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.component_template_delete,
|
|
||||||
{'model': ConsoleServerPortTemplate}, name='devicetype_delete_consoleserverport'),
|
# Power port templates
|
||||||
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(),
|
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), name='devicetype_add_powerport'),
|
||||||
name='devicetype_add_powerport'),
|
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.component_template_delete,
|
|
||||||
{'model': PowerPortTemplate}, name='devicetype_delete_powerport'),
|
# Power outlet templates
|
||||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(),
|
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), name='devicetype_add_poweroutlet'),
|
||||||
name='devicetype_add_poweroutlet'),
|
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.component_template_delete,
|
|
||||||
{'model': PowerOutletTemplate}, name='devicetype_delete_poweroutlet'),
|
# Interface templates
|
||||||
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(),
|
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
|
||||||
name='devicetype_add_interface'),
|
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.component_template_delete,
|
|
||||||
{'model': InterfaceTemplate}, name='devicetype_delete_interface'),
|
# Device bay templates
|
||||||
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(),
|
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), name='devicetype_add_devicebay'),
|
||||||
name='devicetype_add_devicebay'),
|
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.component_template_delete,
|
|
||||||
{'model': DeviceBayTemplate}, name='devicetype_delete_devicebay'),
|
|
||||||
|
|
||||||
# Device roles
|
# Device roles
|
||||||
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||||
@@ -92,6 +91,7 @@ urlpatterns = [
|
|||||||
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
||||||
url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'),
|
url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'),
|
||||||
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
|
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||||
|
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
||||||
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||||
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||||
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
|
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
|
||||||
@@ -104,6 +104,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Console ports
|
# Console ports
|
||||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
|
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, 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.consoleport_connect, name='consoleport_connect'),
|
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+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
||||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.consoleport_edit, name='consoleport_edit'),
|
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.consoleport_edit, name='consoleport_edit'),
|
||||||
@@ -111,6 +112,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Console server ports
|
# Console server ports
|
||||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
|
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
|
||||||
|
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.consoleserverport_connect, name='consoleserverport_connect'),
|
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+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
||||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.consoleserverport_edit, name='consoleserverport_edit'),
|
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.consoleserverport_edit, name='consoleserverport_edit'),
|
||||||
@@ -118,6 +120,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Power ports
|
# Power ports
|
||||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, name='powerport_add'),
|
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, 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.powerport_connect, name='powerport_connect'),
|
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+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
||||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.powerport_edit, name='powerport_edit'),
|
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.powerport_edit, name='powerport_edit'),
|
||||||
@@ -125,6 +128,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Power outlets
|
# Power outlets
|
||||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
|
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
|
||||||
|
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.poweroutlet_connect, name='poweroutlet_connect'),
|
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+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
||||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
|
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
|
||||||
@@ -132,6 +136,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Device bays
|
# Device bays
|
||||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
|
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
|
||||||
|
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.devicebay_edit, name='devicebay_edit'),
|
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.devicebay_edit, name='devicebay_edit'),
|
||||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.devicebay_delete, name='devicebay_delete'),
|
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.devicebay_delete, name='devicebay_delete'),
|
||||||
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
|
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
|
||||||
@@ -146,8 +151,9 @@ urlpatterns = [
|
|||||||
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
||||||
|
|
||||||
# Interfaces
|
# Interfaces
|
||||||
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_bulk_add'),
|
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_add_multi'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
||||||
|
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.interfaceconnection_add, name='interfaceconnection_add'),
|
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'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.interface_edit, name='interface_edit'),
|
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.interface_edit, name='interface_edit'),
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import re
|
import re
|
||||||
|
from natsort import natsorted
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import permission_required
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db.models import Count, ProtectedError
|
from django.db.models import Count, Sum
|
||||||
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
|
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
@@ -14,8 +15,7 @@ from django.views.generic import View
|
|||||||
|
|
||||||
from ipam.models import Prefix, IPAddress, VLAN
|
from ipam.models import Prefix, IPAddress, VLAN
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.models import TopologyMap
|
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||||
from utilities.error_handlers import handle_protectederror
|
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
@@ -61,9 +61,11 @@ def expand_pattern(string):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class SiteListView(ObjectListView):
|
class SiteListView(ObjectListView):
|
||||||
queryset = Site.objects.all()
|
queryset = Site.objects.select_related('tenant')
|
||||||
filter = filters.SiteFilter
|
filter = filters.SiteFilter
|
||||||
|
filter_form = forms.SiteFilterForm
|
||||||
table = tables.SiteTable
|
table = tables.SiteTable
|
||||||
|
edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
|
||||||
template_name = 'dcim/site_list.html'
|
template_name = 'dcim/site_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -79,12 +81,14 @@ def site(request, slug):
|
|||||||
}
|
}
|
||||||
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
||||||
topology_maps = TopologyMap.objects.filter(site=site)
|
topology_maps = TopologyMap.objects.filter(site=site)
|
||||||
|
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
|
||||||
|
|
||||||
return render(request, 'dcim/site.html', {
|
return render(request, 'dcim/site.html', {
|
||||||
'site': site,
|
'site': site,
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'rack_groups': rack_groups,
|
'rack_groups': rack_groups,
|
||||||
'topology_maps': topology_maps,
|
'topology_maps': topology_maps,
|
||||||
|
'show_graphs': show_graphs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -110,6 +114,24 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
obj_list_url = 'dcim:site_list'
|
obj_list_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
|
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'dcim.change_site'
|
||||||
|
cls = Site
|
||||||
|
form = forms.SiteBulkEditForm
|
||||||
|
template_name = 'dcim/site_bulk_edit.html'
|
||||||
|
default_redirect_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
def update_objects(self, pk_list, form):
|
||||||
|
|
||||||
|
fields_to_update = {}
|
||||||
|
if form.cleaned_data['tenant'] == 0:
|
||||||
|
fields_to_update['tenant'] = None
|
||||||
|
elif form.cleaned_data['tenant']:
|
||||||
|
fields_to_update['tenant'] = form.cleaned_data['tenant']
|
||||||
|
|
||||||
|
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack groups
|
# Rack groups
|
||||||
#
|
#
|
||||||
@@ -133,7 +155,6 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_rackgroup'
|
permission_required = 'dcim.delete_rackgroup'
|
||||||
cls = RackGroup
|
cls = RackGroup
|
||||||
form = forms.RackGroupBulkDeleteForm
|
|
||||||
default_redirect_url = 'dcim:rackgroup_list'
|
default_redirect_url = 'dcim:rackgroup_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -142,7 +163,8 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackListView(ObjectListView):
|
class RackListView(ObjectListView):
|
||||||
queryset = Rack.objects.select_related('site').annotate(device_count=Count('devices', distinct=True))
|
queryset = Rack.objects.select_related('site').prefetch_related('devices__device_type')\
|
||||||
|
.annotate(device_count=Count('devices', distinct=True), u_consumed=Sum('devices__device_type__u_height'))
|
||||||
filter = filters.RackFilter
|
filter = filters.RackFilter
|
||||||
filter_form = forms.RackFilterForm
|
filter_form = forms.RackFilterForm
|
||||||
table = tables.RackTable
|
table = tables.RackTable
|
||||||
@@ -154,7 +176,7 @@ def rack(request, pk):
|
|||||||
|
|
||||||
rack = get_object_or_404(Rack, pk=pk)
|
rack = get_object_or_404(Rack, pk=pk)
|
||||||
|
|
||||||
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True)\
|
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
|
||||||
.select_related('device_type__manufacturer')
|
.select_related('device_type__manufacturer')
|
||||||
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
|
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
|
||||||
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
||||||
@@ -186,7 +208,7 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_rack'
|
permission_required = 'dcim.add_rack'
|
||||||
form = forms.RackImportForm
|
form = forms.RackImportForm
|
||||||
table = tables.RackTable
|
table = tables.RackImportTable
|
||||||
template_name = 'dcim/rack_import.html'
|
template_name = 'dcim/rack_import.html'
|
||||||
obj_list_url = 'dcim:rack_list'
|
obj_list_url = 'dcim:rack_list'
|
||||||
|
|
||||||
@@ -201,7 +223,11 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form):
|
||||||
|
|
||||||
fields_to_update = {}
|
fields_to_update = {}
|
||||||
for field in ['site', 'group', 'u_height', 'comments']:
|
if form.cleaned_data['tenant'] == 0:
|
||||||
|
fields_to_update['tenant'] = None
|
||||||
|
elif form.cleaned_data['tenant']:
|
||||||
|
fields_to_update['tenant'] = form.cleaned_data['tenant']
|
||||||
|
for field in ['site', 'group', 'tenant', 'u_height', 'comments']:
|
||||||
if form.cleaned_data[field]:
|
if form.cleaned_data[field]:
|
||||||
fields_to_update[field] = form.cleaned_data[field]
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
|
|
||||||
@@ -211,7 +237,6 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_rack'
|
permission_required = 'dcim.delete_rack'
|
||||||
cls = Rack
|
cls = Rack
|
||||||
form = forms.RackBulkDeleteForm
|
|
||||||
default_redirect_url = 'dcim:rack_list'
|
default_redirect_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -237,7 +262,6 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_manufacturer'
|
permission_required = 'dcim.delete_manufacturer'
|
||||||
cls = Manufacturer
|
cls = Manufacturer
|
||||||
form = forms.ManufacturerBulkDeleteForm
|
|
||||||
default_redirect_url = 'dcim:manufacturer_list'
|
default_redirect_url = 'dcim:manufacturer_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -259,18 +283,31 @@ def devicetype(request, pk):
|
|||||||
devicetype = get_object_or_404(DeviceType, pk=pk)
|
devicetype = get_object_or_404(DeviceType, pk=pk)
|
||||||
|
|
||||||
# Component tables
|
# Component tables
|
||||||
consoleport_table = tables.ConsolePortTemplateTable(ConsolePortTemplate.objects.filter(device_type=devicetype))
|
consoleport_table = tables.ConsolePortTemplateTable(
|
||||||
consoleserverport_table = tables.ConsoleServerPortTemplateTable(ConsoleServerPortTemplate.objects
|
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
.filter(device_type=devicetype))
|
)
|
||||||
powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype))
|
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
|
||||||
poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype))
|
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
|
)
|
||||||
devicebay_table = tables.DeviceBayTemplateTable(DeviceBayTemplate.objects.filter(device_type=devicetype))
|
powerport_table = tables.PowerPortTemplateTable(
|
||||||
|
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
|
)
|
||||||
|
poweroutlet_table = tables.PowerOutletTemplateTable(
|
||||||
|
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
|
)
|
||||||
|
mgmt_interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
|
||||||
|
mgmt_only=True))
|
||||||
|
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
|
||||||
|
mgmt_only=False))
|
||||||
|
devicebay_table = tables.DeviceBayTemplateTable(
|
||||||
|
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
|
)
|
||||||
if request.user.has_perm('dcim.change_devicetype'):
|
if request.user.has_perm('dcim.change_devicetype'):
|
||||||
consoleport_table.base_columns['pk'].visible = True
|
consoleport_table.base_columns['pk'].visible = True
|
||||||
consoleserverport_table.base_columns['pk'].visible = True
|
consoleserverport_table.base_columns['pk'].visible = True
|
||||||
powerport_table.base_columns['pk'].visible = True
|
powerport_table.base_columns['pk'].visible = True
|
||||||
poweroutlet_table.base_columns['pk'].visible = True
|
poweroutlet_table.base_columns['pk'].visible = True
|
||||||
|
mgmt_interface_table.base_columns['pk'].visible = True
|
||||||
interface_table.base_columns['pk'].visible = True
|
interface_table.base_columns['pk'].visible = True
|
||||||
devicebay_table.base_columns['pk'].visible = True
|
devicebay_table.base_columns['pk'].visible = True
|
||||||
|
|
||||||
@@ -280,6 +317,7 @@ def devicetype(request, pk):
|
|||||||
'consoleserverport_table': consoleserverport_table,
|
'consoleserverport_table': consoleserverport_table,
|
||||||
'powerport_table': powerport_table,
|
'powerport_table': powerport_table,
|
||||||
'poweroutlet_table': poweroutlet_table,
|
'poweroutlet_table': poweroutlet_table,
|
||||||
|
'mgmt_interface_table': mgmt_interface_table,
|
||||||
'interface_table': interface_table,
|
'interface_table': interface_table,
|
||||||
'devicebay_table': devicebay_table,
|
'devicebay_table': devicebay_table,
|
||||||
})
|
})
|
||||||
@@ -318,7 +356,6 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_devicetype'
|
permission_required = 'dcim.delete_devicetype'
|
||||||
cls = DeviceType
|
cls = DeviceType
|
||||||
form = forms.DeviceTypeBulkDeleteForm
|
|
||||||
default_redirect_url = 'dcim:devicetype_list'
|
default_redirect_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -337,7 +374,7 @@ class ComponentTemplateCreateView(View):
|
|||||||
return render(request, 'dcim/component_template_add.html', {
|
return render(request, 'dcim/component_template_add.html', {
|
||||||
'devicetype': devicetype,
|
'devicetype': devicetype,
|
||||||
'component_type': self.model._meta.verbose_name,
|
'component_type': self.model._meta.verbose_name,
|
||||||
'form': self.form(),
|
'form': self.form(initial=request.GET),
|
||||||
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
|
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -380,68 +417,65 @@ class ConsolePortTemplateAddView(ComponentTemplateCreateView):
|
|||||||
form = forms.ConsolePortTemplateForm
|
form = forms.ConsolePortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_consoleporttemplate'
|
||||||
|
cls = ConsolePortTemplate
|
||||||
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateAddView(ComponentTemplateCreateView):
|
class ConsoleServerPortTemplateAddView(ComponentTemplateCreateView):
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
form = forms.ConsoleServerPortTemplateForm
|
form = forms.ConsoleServerPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_consoleserverporttemplate'
|
||||||
|
cls = ConsoleServerPortTemplate
|
||||||
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateAddView(ComponentTemplateCreateView):
|
class PowerPortTemplateAddView(ComponentTemplateCreateView):
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
form = forms.PowerPortTemplateForm
|
form = forms.PowerPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_powerporttemplate'
|
||||||
|
cls = PowerPortTemplate
|
||||||
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateAddView(ComponentTemplateCreateView):
|
class PowerOutletTemplateAddView(ComponentTemplateCreateView):
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
form = forms.PowerOutletTemplateForm
|
form = forms.PowerOutletTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_poweroutlettemplate'
|
||||||
|
cls = PowerOutletTemplate
|
||||||
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateAddView(ComponentTemplateCreateView):
|
class InterfaceTemplateAddView(ComponentTemplateCreateView):
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
form = forms.InterfaceTemplateForm
|
form = forms.InterfaceTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_interfacetemplate'
|
||||||
|
cls = InterfaceTemplate
|
||||||
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateAddView(ComponentTemplateCreateView):
|
class DeviceBayTemplateAddView(ComponentTemplateCreateView):
|
||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
form = forms.DeviceBayTemplateForm
|
form = forms.DeviceBayTemplateForm
|
||||||
|
|
||||||
|
|
||||||
def component_template_delete(request, pk, model):
|
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_devicebaytemplate'
|
||||||
devicetype = get_object_or_404(DeviceType, pk=pk)
|
cls = DeviceBayTemplate
|
||||||
|
parent_cls = DeviceType
|
||||||
class ComponentTemplateBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = ModelMultipleChoiceField(queryset=model.objects.all(), widget=MultipleHiddenInput)
|
|
||||||
|
|
||||||
if '_confirm' in request.POST:
|
|
||||||
form = ComponentTemplateBulkDeleteForm(request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
|
|
||||||
# Delete component templates
|
|
||||||
objects_to_delete = model.objects.filter(pk__in=[v.id for v in form.cleaned_data['pk']])
|
|
||||||
try:
|
|
||||||
deleted_count = objects_to_delete.count()
|
|
||||||
objects_to_delete.delete()
|
|
||||||
except ProtectedError, e:
|
|
||||||
handle_protectederror(list(objects_to_delete), request, e)
|
|
||||||
return redirect('dcim:devicetype', {'pk': devicetype.pk})
|
|
||||||
|
|
||||||
messages.success(request, "Deleted {} {}".format(deleted_count, model._meta.verbose_name_plural))
|
|
||||||
return redirect('dcim:devicetype', pk=devicetype.pk)
|
|
||||||
|
|
||||||
else:
|
|
||||||
form = ComponentTemplateBulkDeleteForm(initial={'pk': request.POST.getlist('pk')})
|
|
||||||
|
|
||||||
selected_objects = model.objects.filter(pk__in=request.POST.getlist('pk'))
|
|
||||||
if not selected_objects:
|
|
||||||
messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
|
|
||||||
return redirect('dcim:devicetype', pk=devicetype.pk)
|
|
||||||
|
|
||||||
return render(request, 'dcim/component_template_delete.html', {
|
|
||||||
'devicetype': devicetype,
|
|
||||||
'form': form,
|
|
||||||
'selected_objects': selected_objects,
|
|
||||||
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -466,7 +500,6 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_devicerole'
|
permission_required = 'dcim.delete_devicerole'
|
||||||
cls = DeviceRole
|
cls = DeviceRole
|
||||||
form = forms.DeviceRoleBulkDeleteForm
|
|
||||||
default_redirect_url = 'dcim:devicerole_list'
|
default_redirect_url = 'dcim:devicerole_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -492,7 +525,6 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_platform'
|
permission_required = 'dcim.delete_platform'
|
||||||
cls = Platform
|
cls = Platform
|
||||||
form = forms.PlatformBulkDeleteForm
|
|
||||||
default_redirect_url = 'dcim:platform_list'
|
default_redirect_url = 'dcim:platform_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -513,15 +545,26 @@ class DeviceListView(ObjectListView):
|
|||||||
def device(request, pk):
|
def device(request, pk):
|
||||||
|
|
||||||
device = get_object_or_404(Device, pk=pk)
|
device = get_object_or_404(Device, pk=pk)
|
||||||
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
|
console_ports = natsorted(
|
||||||
cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
|
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
|
||||||
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
|
)
|
||||||
power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port')
|
cs_ports = natsorted(
|
||||||
|
ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
|
||||||
|
)
|
||||||
|
power_ports = natsorted(
|
||||||
|
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
|
||||||
|
)
|
||||||
|
power_outlets = natsorted(
|
||||||
|
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||||
|
)
|
||||||
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
|
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
|
||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||||
device_bays = DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer')
|
device_bays = natsorted(
|
||||||
|
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||||
|
key=attrgetter('name')
|
||||||
|
)
|
||||||
|
|
||||||
# Gather any secrets which belong to this device
|
# Gather any secrets which belong to this device
|
||||||
secrets = device.secrets.all()
|
secrets = device.secrets.all()
|
||||||
@@ -544,6 +587,9 @@ def device(request, pk):
|
|||||||
related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
|
related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
|
||||||
.select_related('rack', 'device_type__manufacturer')[:10]
|
.select_related('rack', 'device_type__manufacturer')[:10]
|
||||||
|
|
||||||
|
# Show graph button on interfaces only if at least one graph has been created.
|
||||||
|
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
|
||||||
|
|
||||||
return render(request, 'dcim/device.html', {
|
return render(request, 'dcim/device.html', {
|
||||||
'device': device,
|
'device': device,
|
||||||
'console_ports': console_ports,
|
'console_ports': console_ports,
|
||||||
@@ -556,6 +602,7 @@ def device(request, pk):
|
|||||||
'ip_addresses': ip_addresses,
|
'ip_addresses': ip_addresses,
|
||||||
'secrets': secrets,
|
'secrets': secrets,
|
||||||
'related_devices': related_devices,
|
'related_devices': related_devices,
|
||||||
|
'show_graphs': show_graphs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -582,6 +629,23 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
obj_list_url = 'dcim:device_list'
|
obj_list_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
|
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
|
permission_required = 'dcim.add_device'
|
||||||
|
form = forms.ChildDeviceImportForm
|
||||||
|
table = tables.DeviceImportTable
|
||||||
|
template_name = 'dcim/device_import_child.html'
|
||||||
|
obj_list_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
def save_obj(self, obj):
|
||||||
|
# Inherent rack from parent device
|
||||||
|
obj.rack = obj.parent_bay.device.rack
|
||||||
|
obj.save()
|
||||||
|
# Save the reverse relation
|
||||||
|
device_bay = obj.parent_bay
|
||||||
|
device_bay.installed_device = obj
|
||||||
|
device_bay.save()
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_device'
|
permission_required = 'dcim.change_device'
|
||||||
cls = Device
|
cls = Device
|
||||||
@@ -592,14 +656,15 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form):
|
||||||
|
|
||||||
fields_to_update = {}
|
fields_to_update = {}
|
||||||
if form.cleaned_data['platform']:
|
for field in ['tenant', 'platform']:
|
||||||
fields_to_update['platform'] = form.cleaned_data['platform']
|
if form.cleaned_data[field] == 0:
|
||||||
elif form.cleaned_data['platform_delete']:
|
fields_to_update[field] = None
|
||||||
fields_to_update['platform'] = None
|
elif form.cleaned_data[field]:
|
||||||
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
if form.cleaned_data['status']:
|
if form.cleaned_data['status']:
|
||||||
status = form.cleaned_data['status']
|
status = form.cleaned_data['status']
|
||||||
fields_to_update['status'] = True if status == 'True' else False
|
fields_to_update['status'] = True if status == 'True' else False
|
||||||
for field in ['device_type', 'device_role', 'serial']:
|
for field in ['tenant', 'device_type', 'device_role', 'serial']:
|
||||||
if form.cleaned_data[field]:
|
if form.cleaned_data[field]:
|
||||||
fields_to_update[field] = form.cleaned_data[field]
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
|
|
||||||
@@ -609,7 +674,6 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_device'
|
permission_required = 'dcim.delete_device'
|
||||||
cls = Device
|
cls = Device
|
||||||
form = forms.DeviceBulkDeleteForm
|
|
||||||
default_redirect_url = 'dcim:device_list'
|
default_redirect_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -781,6 +845,12 @@ def consoleport_delete(request, pk):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_consoleport'
|
||||||
|
cls = ConsolePort
|
||||||
|
parent_cls = Device
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.change_consoleport'
|
permission_required = 'dcim.change_consoleport'
|
||||||
form = forms.ConsoleConnectionImportForm
|
form = forms.ConsoleConnectionImportForm
|
||||||
@@ -936,6 +1006,12 @@ def consoleserverport_delete(request, pk):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_consoleserverport'
|
||||||
|
cls = ConsoleServerPort
|
||||||
|
parent_cls = Device
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power ports
|
# Power ports
|
||||||
#
|
#
|
||||||
@@ -1081,6 +1157,12 @@ def powerport_delete(request, pk):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_powerport'
|
||||||
|
cls = PowerPort
|
||||||
|
parent_cls = Device
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.change_powerport'
|
permission_required = 'dcim.change_powerport'
|
||||||
form = forms.PowerConnectionImportForm
|
form = forms.PowerConnectionImportForm
|
||||||
@@ -1234,6 +1316,12 @@ def poweroutlet_delete(request, pk):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_poweroutlet'
|
||||||
|
cls = PowerOutlet
|
||||||
|
parent_cls = Device
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
@@ -1328,7 +1416,7 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
|||||||
permission_required = 'dcim.add_interface'
|
permission_required = 'dcim.add_interface'
|
||||||
cls = Device
|
cls = Device
|
||||||
form = forms.InterfaceBulkCreateForm
|
form = forms.InterfaceBulkCreateForm
|
||||||
template_name = 'dcim/interface_bulk_add.html'
|
template_name = 'dcim/interface_add_multi.html'
|
||||||
default_redirect_url = 'dcim:device_list'
|
default_redirect_url = 'dcim:device_list'
|
||||||
|
|
||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form):
|
||||||
@@ -1357,6 +1445,12 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
|||||||
len(selected_devices)))
|
len(selected_devices)))
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_interface'
|
||||||
|
cls = Interface
|
||||||
|
parent_cls = Device
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device bays
|
# Device bays
|
||||||
#
|
#
|
||||||
@@ -1494,6 +1588,12 @@ def devicebay_depopulate(request, pk):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_devicebay'
|
||||||
|
cls = DeviceBay
|
||||||
|
parent_cls = Device
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interface connections
|
# Interface connections
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -19,3 +19,9 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
|||||||
prepopulated_fields = {
|
prepopulated_fields = {
|
||||||
'slug': ['name'],
|
'slug': ['name'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserAction)
|
||||||
|
class UserActionAdmin(admin.ModelAdmin):
|
||||||
|
actions = None
|
||||||
|
list_display = ['user', 'action', 'content_type', 'object_id', 'message']
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class ExportTemplate(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "{}: {}".format(self.content_type, self.name)
|
return u'{}: {}'.format(self.content_type, self.name)
|
||||||
|
|
||||||
def to_response(self, context_dict, filename):
|
def to_response(self, context_dict, filename):
|
||||||
"""
|
"""
|
||||||
@@ -176,8 +176,8 @@ class UserAction(models.Model):
|
|||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.message:
|
if self.message:
|
||||||
return ' '.join([self.user, self.message])
|
return u'{} {}'.format(self.user, self.message)
|
||||||
return ' '.join([self.user, self.get_action_display(), self.content_type])
|
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
if self.action in [ACTION_CREATE, ACTION_IMPORT]:
|
if self.action in [ACTION_CREATE, ACTION_IMPORT]:
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF,
|
Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(VRF)
|
@admin.register(VRF)
|
||||||
class VRFAdmin(admin.ModelAdmin):
|
class VRFAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'rd']
|
list_display = ['name', 'rd', 'tenant', 'enforce_unique']
|
||||||
|
list_filter = ['tenant']
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super(VRFAdmin, self).get_queryset(request)
|
||||||
|
return qs.select_related('tenant')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Role)
|
@admin.register(Role)
|
||||||
@@ -35,7 +40,7 @@ class AggregateAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Prefix)
|
@admin.register(Prefix)
|
||||||
class PrefixAdmin(admin.ModelAdmin):
|
class PrefixAdmin(admin.ModelAdmin):
|
||||||
list_display = ['prefix', 'vrf', 'site', 'status', 'role', 'vlan']
|
list_display = ['prefix', 'vrf', 'tenant', 'site', 'status', 'role', 'vlan']
|
||||||
list_filter = ['family', 'site', 'status', 'role']
|
list_filter = ['family', 'site', 'status', 'role']
|
||||||
search_fields = ['prefix']
|
search_fields = ['prefix']
|
||||||
|
|
||||||
@@ -46,7 +51,7 @@ class PrefixAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(IPAddress)
|
@admin.register(IPAddress)
|
||||||
class IPAddressAdmin(admin.ModelAdmin):
|
class IPAddressAdmin(admin.ModelAdmin):
|
||||||
list_display = ['address', 'vrf', 'nat_inside']
|
list_display = ['address', 'vrf', 'tenant', 'nat_inside']
|
||||||
list_filter = ['family']
|
list_filter = ['family']
|
||||||
fields = ['address', 'vrf', 'device', 'interface', 'nat_inside']
|
fields = ['address', 'vrf', 'device', 'interface', 'nat_inside']
|
||||||
readonly_fields = ['interface', 'device', 'nat_inside']
|
readonly_fields = ['interface', 'device', 'nat_inside']
|
||||||
@@ -57,12 +62,20 @@ class IPAddressAdmin(admin.ModelAdmin):
|
|||||||
return qs.select_related('vrf', 'nat_inside')
|
return qs.select_related('vrf', 'nat_inside')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(VLANGroup)
|
||||||
|
class VLANGroupAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'site', 'slug']
|
||||||
|
prepopulated_fields = {
|
||||||
|
'slug': ['name'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@admin.register(VLAN)
|
@admin.register(VLAN)
|
||||||
class VLANAdmin(admin.ModelAdmin):
|
class VLANAdmin(admin.ModelAdmin):
|
||||||
list_display = ['site', 'vid', 'name', 'status', 'role']
|
list_display = ['site', 'vid', 'name', 'tenant', 'status', 'role']
|
||||||
list_filter = ['site', 'status', 'role']
|
list_filter = ['site', 'tenant', 'status', 'role']
|
||||||
search_fields = ['vid', 'name']
|
search_fields = ['vid', 'name']
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(VLANAdmin, self).get_queryset(request)
|
qs = super(VLANAdmin, self).get_queryset(request)
|
||||||
return qs.select_related('site', 'role')
|
return qs.select_related('site', 'tenant', 'role')
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||||
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
|
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
|
||||||
|
from tenancy.api.serializers import TenantNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -9,10 +10,11 @@ from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VRFSerializer(serializers.ModelSerializer):
|
class VRFSerializer(serializers.ModelSerializer):
|
||||||
|
tenant = TenantNestedSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['id', 'name', 'rd', 'description']
|
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||||
|
|
||||||
|
|
||||||
class VRFNestedSerializer(VRFSerializer):
|
class VRFNestedSerializer(VRFSerializer):
|
||||||
@@ -21,6 +23,15 @@ class VRFNestedSerializer(VRFSerializer):
|
|||||||
fields = ['id', 'name', 'rd']
|
fields = ['id', 'name', 'rd']
|
||||||
|
|
||||||
|
|
||||||
|
class VRFTenantSerializer(VRFSerializer):
|
||||||
|
"""
|
||||||
|
Include tenant serializer. Useful for determining tenant inheritance for Prefixes and IPAddresses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta(VRFSerializer.Meta):
|
||||||
|
fields = ['id', 'name', 'rd', 'tenant']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Roles
|
# Roles
|
||||||
#
|
#
|
||||||
@@ -73,17 +84,37 @@ class AggregateNestedSerializer(AggregateSerializer):
|
|||||||
fields = ['id', 'family', 'prefix']
|
fields = ['id', 'family', 'prefix']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# VLAN groups
|
||||||
|
#
|
||||||
|
|
||||||
|
class VLANGroupSerializer(serializers.ModelSerializer):
|
||||||
|
site = SiteNestedSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANGroup
|
||||||
|
fields = ['id', 'name', 'slug', 'site']
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupNestedSerializer(VLANGroupSerializer):
|
||||||
|
|
||||||
|
class Meta(VLANGroupSerializer.Meta):
|
||||||
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VLANSerializer(serializers.ModelSerializer):
|
class VLANSerializer(serializers.ModelSerializer):
|
||||||
site = SiteNestedSerializer()
|
site = SiteNestedSerializer()
|
||||||
|
group = VLANGroupNestedSerializer()
|
||||||
|
tenant = TenantNestedSerializer()
|
||||||
role = RoleNestedSerializer()
|
role = RoleNestedSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['id', 'site', 'vid', 'name', 'status', 'role', 'display_name']
|
fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name']
|
||||||
|
|
||||||
|
|
||||||
class VLANNestedSerializer(VLANSerializer):
|
class VLANNestedSerializer(VLANSerializer):
|
||||||
@@ -98,13 +129,14 @@ class VLANNestedSerializer(VLANSerializer):
|
|||||||
|
|
||||||
class PrefixSerializer(serializers.ModelSerializer):
|
class PrefixSerializer(serializers.ModelSerializer):
|
||||||
site = SiteNestedSerializer()
|
site = SiteNestedSerializer()
|
||||||
vrf = VRFNestedSerializer()
|
vrf = VRFTenantSerializer()
|
||||||
|
tenant = TenantNestedSerializer()
|
||||||
vlan = VLANNestedSerializer()
|
vlan = VLANNestedSerializer()
|
||||||
role = RoleNestedSerializer()
|
role = RoleNestedSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'vlan', 'status', 'role', 'description']
|
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description']
|
||||||
|
|
||||||
|
|
||||||
class PrefixNestedSerializer(PrefixSerializer):
|
class PrefixNestedSerializer(PrefixSerializer):
|
||||||
@@ -118,12 +150,13 @@ class PrefixNestedSerializer(PrefixSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class IPAddressSerializer(serializers.ModelSerializer):
|
class IPAddressSerializer(serializers.ModelSerializer):
|
||||||
vrf = VRFNestedSerializer()
|
vrf = VRFTenantSerializer()
|
||||||
|
tenant = TenantNestedSerializer()
|
||||||
interface = InterfaceNestedSerializer()
|
interface = InterfaceNestedSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['id', 'family', 'address', 'vrf', 'interface', 'description', 'nat_inside', 'nat_outside']
|
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside']
|
||||||
|
|
||||||
|
|
||||||
class IPAddressNestedSerializer(IPAddressSerializer):
|
class IPAddressNestedSerializer(IPAddressSerializer):
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ urlpatterns = [
|
|||||||
url(r'^ip-addresses/$', IPAddressListView.as_view(), name='ipaddress_list'),
|
url(r'^ip-addresses/$', IPAddressListView.as_view(), name='ipaddress_list'),
|
||||||
url(r'^ip-addresses/(?P<pk>\d+)/$', IPAddressDetailView.as_view(), name='ipaddress_detail'),
|
url(r'^ip-addresses/(?P<pk>\d+)/$', IPAddressDetailView.as_view(), name='ipaddress_detail'),
|
||||||
|
|
||||||
|
# VLAN groups
|
||||||
|
url(r'^vlan-groups/$', VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||||
|
url(r'^vlan-groups/(?P<pk>\d+)/$', VLANGroupDetailView.as_view(), name='vlangroup_detail'),
|
||||||
|
|
||||||
# VLANs
|
# VLANs
|
||||||
url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'),
|
url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'),
|
||||||
url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),
|
url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),
|
||||||
|
|||||||
@@ -1,28 +1,36 @@
|
|||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
|
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
|
||||||
from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter, VRFFilter
|
from ipam import filters
|
||||||
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# VRFs
|
||||||
|
#
|
||||||
|
|
||||||
class VRFListView(generics.ListAPIView):
|
class VRFListView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
List all VRFs
|
List all VRFs
|
||||||
"""
|
"""
|
||||||
queryset = VRF.objects.all()
|
queryset = VRF.objects.select_related('tenant')
|
||||||
serializer_class = serializers.VRFSerializer
|
serializer_class = serializers.VRFSerializer
|
||||||
filter_class = VRFFilter
|
filter_class = filters.VRFFilter
|
||||||
|
|
||||||
|
|
||||||
class VRFDetailView(generics.RetrieveAPIView):
|
class VRFDetailView(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
Retrieve a single VRF
|
Retrieve a single VRF
|
||||||
"""
|
"""
|
||||||
queryset = VRF.objects.all()
|
queryset = VRF.objects.select_related('tenant')
|
||||||
serializer_class = serializers.VRFSerializer
|
serializer_class = serializers.VRFSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Roles
|
||||||
|
#
|
||||||
|
|
||||||
class RoleListView(generics.ListAPIView):
|
class RoleListView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
List all roles
|
List all roles
|
||||||
@@ -39,6 +47,10 @@ class RoleDetailView(generics.RetrieveAPIView):
|
|||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# RIRs
|
||||||
|
#
|
||||||
|
|
||||||
class RIRListView(generics.ListAPIView):
|
class RIRListView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
List all RIRs
|
List all RIRs
|
||||||
@@ -55,13 +67,17 @@ class RIRDetailView(generics.RetrieveAPIView):
|
|||||||
serializer_class = serializers.RIRSerializer
|
serializer_class = serializers.RIRSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Aggregates
|
||||||
|
#
|
||||||
|
|
||||||
class AggregateListView(generics.ListAPIView):
|
class AggregateListView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
List aggregates (filterable)
|
List aggregates (filterable)
|
||||||
"""
|
"""
|
||||||
queryset = Aggregate.objects.select_related('rir')
|
queryset = Aggregate.objects.select_related('rir')
|
||||||
serializer_class = serializers.AggregateSerializer
|
serializer_class = serializers.AggregateSerializer
|
||||||
filter_class = AggregateFilter
|
filter_class = filters.AggregateFilter
|
||||||
|
|
||||||
|
|
||||||
class AggregateDetailView(generics.RetrieveAPIView):
|
class AggregateDetailView(generics.RetrieveAPIView):
|
||||||
@@ -72,54 +88,87 @@ class AggregateDetailView(generics.RetrieveAPIView):
|
|||||||
serializer_class = serializers.AggregateSerializer
|
serializer_class = serializers.AggregateSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Prefixes
|
||||||
|
#
|
||||||
|
|
||||||
class PrefixListView(generics.ListAPIView):
|
class PrefixListView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
List prefixes (filterable)
|
List prefixes (filterable)
|
||||||
"""
|
"""
|
||||||
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
|
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||||
serializer_class = serializers.PrefixSerializer
|
serializer_class = serializers.PrefixSerializer
|
||||||
filter_class = PrefixFilter
|
filter_class = filters.PrefixFilter
|
||||||
|
|
||||||
|
|
||||||
class PrefixDetailView(generics.RetrieveAPIView):
|
class PrefixDetailView(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
Retrieve a single prefix
|
Retrieve a single prefix
|
||||||
"""
|
"""
|
||||||
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
|
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||||
serializer_class = serializers.PrefixSerializer
|
serializer_class = serializers.PrefixSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# IP addresses
|
||||||
|
#
|
||||||
|
|
||||||
class IPAddressListView(generics.ListAPIView):
|
class IPAddressListView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
List IP addresses (filterable)
|
List IP addresses (filterable)
|
||||||
"""
|
"""
|
||||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
|
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
|
||||||
.prefetch_related('nat_outside')
|
.prefetch_related('nat_outside')
|
||||||
serializer_class = serializers.IPAddressSerializer
|
serializer_class = serializers.IPAddressSerializer
|
||||||
filter_class = IPAddressFilter
|
filter_class = filters.IPAddressFilter
|
||||||
|
|
||||||
|
|
||||||
class IPAddressDetailView(generics.RetrieveAPIView):
|
class IPAddressDetailView(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
Retrieve a single IP address
|
Retrieve a single IP address
|
||||||
"""
|
"""
|
||||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
|
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
|
||||||
.prefetch_related('nat_outside')
|
.prefetch_related('nat_outside')
|
||||||
serializer_class = serializers.IPAddressSerializer
|
serializer_class = serializers.IPAddressSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# VLAN groups
|
||||||
|
#
|
||||||
|
|
||||||
|
class VLANGroupListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
List all VLAN groups
|
||||||
|
"""
|
||||||
|
queryset = VLANGroup.objects.select_related('site')
|
||||||
|
serializer_class = serializers.VLANGroupSerializer
|
||||||
|
filter_class = filters.VLANGroupFilter
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupDetailView(generics.RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
Retrieve a single VLAN group
|
||||||
|
"""
|
||||||
|
queryset = VLANGroup.objects.select_related('site')
|
||||||
|
serializer_class = serializers.VLANGroupSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# VLANs
|
||||||
|
#
|
||||||
|
|
||||||
class VLANListView(generics.ListAPIView):
|
class VLANListView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
List VLANs (filterable)
|
List VLANs (filterable)
|
||||||
"""
|
"""
|
||||||
queryset = VLAN.objects.select_related('site', 'role')
|
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||||
serializer_class = serializers.VLANSerializer
|
serializer_class = serializers.VLANSerializer
|
||||||
filter_class = VLANFilter
|
filter_class = filters.VLANFilter
|
||||||
|
|
||||||
|
|
||||||
class VLANDetailView(generics.RetrieveAPIView):
|
class VLANDetailView(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
Retrieve a single VLAN
|
Retrieve a single VLAN
|
||||||
"""
|
"""
|
||||||
queryset = VLAN.objects.select_related('site', 'role')
|
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||||
serializer_class = serializers.VLANSerializer
|
serializer_class = serializers.VLANSerializer
|
||||||
|
|||||||
@@ -2,17 +2,42 @@ import django_filters
|
|||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
from dcim.models import Site, Device, Interface
|
from django.db.models import Q
|
||||||
|
|
||||||
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, Role
|
from dcim.models import Site, Device, Interface
|
||||||
|
from tenancy.models import Tenant
|
||||||
|
|
||||||
|
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
|
||||||
|
|
||||||
|
|
||||||
class VRFFilter(django_filters.FilterSet):
|
class VRFFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.MethodFilter(
|
||||||
|
action='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
name = django_filters.CharFilter(
|
name = django_filters.CharFilter(
|
||||||
name='name',
|
name='name',
|
||||||
lookup_type='icontains',
|
lookup_type='icontains',
|
||||||
label='Name',
|
label='Name',
|
||||||
)
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
label='Tenant (ID)',
|
||||||
|
)
|
||||||
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
|
)
|
||||||
|
|
||||||
|
def search(self, queryset, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(rd__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
@@ -20,6 +45,10 @@ class VRFFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class AggregateFilter(django_filters.FilterSet):
|
class AggregateFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.MethodFilter(
|
||||||
|
action='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rir',
|
name='rir',
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
@@ -36,6 +65,15 @@ class AggregateFilter(django_filters.FilterSet):
|
|||||||
model = Aggregate
|
model = Aggregate
|
||||||
fields = ['family', 'rir_id', 'rir', 'date_added']
|
fields = ['family', 'rir_id', 'rir', 'date_added']
|
||||||
|
|
||||||
|
def search(self, queryset, value):
|
||||||
|
qs_filter = Q(description__icontains=value)
|
||||||
|
try:
|
||||||
|
prefix = str(IPNetwork(value.strip()).cidr)
|
||||||
|
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||||
|
except AddrFormatError:
|
||||||
|
pass
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class PrefixFilter(django_filters.FilterSet):
|
class PrefixFilter(django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.MethodFilter(
|
||||||
@@ -55,6 +93,14 @@ class PrefixFilter(django_filters.FilterSet):
|
|||||||
action='_vrf',
|
action='_vrf',
|
||||||
label='VRF',
|
label='VRF',
|
||||||
)
|
)
|
||||||
|
tenant_id = django_filters.MethodFilter(
|
||||||
|
action='_tenant_id',
|
||||||
|
label='Tenant (ID)',
|
||||||
|
)
|
||||||
|
tenant = django_filters.MethodFilter(
|
||||||
|
action='_tenant',
|
||||||
|
label='Tenant',
|
||||||
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@@ -92,12 +138,13 @@ class PrefixFilter(django_filters.FilterSet):
|
|||||||
fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
|
fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
value = value.strip()
|
qs_filter = Q(description__icontains=value)
|
||||||
try:
|
try:
|
||||||
query = str(IPNetwork(value).cidr)
|
prefix = str(IPNetwork(value.strip()).cidr)
|
||||||
return queryset.filter(prefix__net_contains_or_equals=query)
|
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
return queryset.none()
|
pass
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
def search_by_parent(self, queryset, value):
|
def search_by_parent(self, queryset, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
@@ -120,12 +167,34 @@ class PrefixFilter(django_filters.FilterSet):
|
|||||||
return queryset.filter(vrf__isnull=True)
|
return queryset.filter(vrf__isnull=True)
|
||||||
return queryset.filter(vrf__pk=value)
|
return queryset.filter(vrf__pk=value)
|
||||||
|
|
||||||
|
def _tenant(self, queryset, value):
|
||||||
|
if str(value) == '':
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(tenant__slug=value) |
|
||||||
|
Q(tenant__isnull=True, vrf__tenant__slug=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _tenant_id(self, queryset, value):
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
except ValueError:
|
||||||
|
return queryset.none()
|
||||||
|
return queryset.filter(
|
||||||
|
Q(tenant__pk=value) |
|
||||||
|
Q(tenant__isnull=True, vrf__tenant__pk=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressFilter(django_filters.FilterSet):
|
class IPAddressFilter(django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.MethodFilter(
|
||||||
action='search',
|
action='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
parent = django_filters.MethodFilter(
|
||||||
|
action='search_by_parent',
|
||||||
|
label='Parent prefix',
|
||||||
|
)
|
||||||
vrf = django_filters.MethodFilter(
|
vrf = django_filters.MethodFilter(
|
||||||
action='_vrf',
|
action='_vrf',
|
||||||
label='VRF',
|
label='VRF',
|
||||||
@@ -135,6 +204,14 @@ class IPAddressFilter(django_filters.FilterSet):
|
|||||||
action='_vrf',
|
action='_vrf',
|
||||||
label='VRF',
|
label='VRF',
|
||||||
)
|
)
|
||||||
|
tenant_id = django_filters.MethodFilter(
|
||||||
|
action='_tenant_id',
|
||||||
|
label='Tenant (ID)',
|
||||||
|
)
|
||||||
|
tenant = django_filters.MethodFilter(
|
||||||
|
action='_tenant',
|
||||||
|
label='Tenant',
|
||||||
|
)
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='interface__device',
|
name='interface__device',
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@@ -157,10 +234,21 @@ class IPAddressFilter(django_filters.FilterSet):
|
|||||||
fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
|
fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
value = value.strip()
|
qs_filter = Q(description__icontains=value)
|
||||||
try:
|
try:
|
||||||
query = str(IPNetwork(value))
|
ipaddress = str(IPNetwork(value.strip()))
|
||||||
return queryset.filter(address__net_host=query)
|
qs_filter |= Q(address__net_host=ipaddress)
|
||||||
|
except AddrFormatError:
|
||||||
|
pass
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
def search_by_parent(self, queryset, value):
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return queryset
|
||||||
|
try:
|
||||||
|
query = str(IPNetwork(value).cidr)
|
||||||
|
return queryset.filter(address__net_contained_or_equal=query)
|
||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
@@ -175,8 +263,26 @@ class IPAddressFilter(django_filters.FilterSet):
|
|||||||
return queryset.filter(vrf__isnull=True)
|
return queryset.filter(vrf__isnull=True)
|
||||||
return queryset.filter(vrf__pk=value)
|
return queryset.filter(vrf__pk=value)
|
||||||
|
|
||||||
|
def _tenant(self, queryset, value):
|
||||||
|
if str(value) == '':
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(tenant__slug=value) |
|
||||||
|
Q(tenant__isnull=True, vrf__tenant__slug=value)
|
||||||
|
)
|
||||||
|
|
||||||
class VLANFilter(django_filters.FilterSet):
|
def _tenant_id(self, queryset, value):
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
except ValueError:
|
||||||
|
return queryset.none()
|
||||||
|
return queryset.filter(
|
||||||
|
Q(tenant__pk=value) |
|
||||||
|
Q(tenant__isnull=True, vrf__tenant__pk=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@@ -188,6 +294,39 @@ class VLANFilter(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANGroup
|
||||||
|
fields = ['site_id', 'site']
|
||||||
|
|
||||||
|
|
||||||
|
class VLANFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.MethodFilter(
|
||||||
|
action='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='site',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
label='Site (ID)',
|
||||||
|
)
|
||||||
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='site',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Site (slug)',
|
||||||
|
)
|
||||||
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='group',
|
||||||
|
queryset=VLANGroup.objects.all(),
|
||||||
|
label='Group (ID)',
|
||||||
|
)
|
||||||
|
group = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='group',
|
||||||
|
queryset=VLANGroup.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Group',
|
||||||
|
)
|
||||||
name = django_filters.CharFilter(
|
name = django_filters.CharFilter(
|
||||||
name='name',
|
name='name',
|
||||||
lookup_type='icontains',
|
lookup_type='icontains',
|
||||||
@@ -197,6 +336,17 @@ class VLANFilter(django_filters.FilterSet):
|
|||||||
name='vid',
|
name='vid',
|
||||||
label='VLAN number (1-4095)',
|
label='VLAN number (1-4095)',
|
||||||
)
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
label='Tenant (ID)',
|
||||||
|
)
|
||||||
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
|
)
|
||||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role',
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
@@ -212,3 +362,11 @@ class VLANFilter(django_filters.FilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site_id', 'site', 'vid', 'name', 'status', 'role_id', 'role']
|
fields = ['site_id', 'site', 'vid', 'name', 'status', 'role_id', 'role']
|
||||||
|
|
||||||
|
def search(self, queryset, value):
|
||||||
|
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||||
|
try:
|
||||||
|
qs_filter |= Q(vid=int(value))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|||||||
125
netbox/ipam/fixtures/initial_data.json
Normal file
125
netbox/ipam/fixtures/initial_data.json
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "ipam.aggregate",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"created": "2016-08-01",
|
||||||
|
"last_updated": "2016-08-01T15:22:20.938Z",
|
||||||
|
"family": 4,
|
||||||
|
"prefix": "10.0.0.0/8",
|
||||||
|
"rir": 6,
|
||||||
|
"date_added": null,
|
||||||
|
"description": "Private IPv4 space"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.aggregate",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"created": "2016-08-01",
|
||||||
|
"last_updated": "2016-08-01T15:22:32.679Z",
|
||||||
|
"family": 4,
|
||||||
|
"prefix": "172.16.0.0/12",
|
||||||
|
"rir": 6,
|
||||||
|
"date_added": null,
|
||||||
|
"description": "Private IPv4 space"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.aggregate",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"created": "2016-08-01",
|
||||||
|
"last_updated": "2016-08-01T15:22:42.289Z",
|
||||||
|
"family": 4,
|
||||||
|
"prefix": "192.168.0.0/16",
|
||||||
|
"rir": 6,
|
||||||
|
"date_added": null,
|
||||||
|
"description": "Private IPv4 space"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.rir",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "ARIN",
|
||||||
|
"slug": "arin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.rir",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "RIPE",
|
||||||
|
"slug": "ripe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.rir",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "APNIC",
|
||||||
|
"slug": "apnic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.rir",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "LACNIC",
|
||||||
|
"slug": "lacnic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.rir",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"name": "AFRINIC",
|
||||||
|
"slug": "afrinic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.rir",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"name": "RFC 1918",
|
||||||
|
"slug": "rfc-1918"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.role",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "Production",
|
||||||
|
"slug": "production",
|
||||||
|
"weight": 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.role",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "Development",
|
||||||
|
"slug": "development",
|
||||||
|
"weight": 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.role",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "Management",
|
||||||
|
"slug": "management",
|
||||||
|
"weight": 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ipam.role",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "Backup",
|
||||||
|
"slug": "backup",
|
||||||
|
"weight": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -4,12 +4,12 @@ from django import forms
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Site, Device, Interface
|
from dcim.models import Site, Device, Interface
|
||||||
from utilities.forms import (
|
from tenancy.forms import bulkedit_tenant_choices
|
||||||
BootstrapMixin, ConfirmationForm, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField,
|
from tenancy.models import Tenant
|
||||||
)
|
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLAN_STATUS_CHOICES, VRF,
|
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -17,6 +17,18 @@ FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
|
|||||||
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
|
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
|
||||||
|
|
||||||
|
|
||||||
|
def bulkedit_vrf_choices():
|
||||||
|
"""
|
||||||
|
Include an option to assign the object to the global table.
|
||||||
|
"""
|
||||||
|
choices = [
|
||||||
|
(None, '---------'),
|
||||||
|
(0, 'Global'),
|
||||||
|
]
|
||||||
|
choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
||||||
|
return choices
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
@@ -25,7 +37,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['name', 'rd', 'description']
|
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||||
labels = {
|
labels = {
|
||||||
'rd': "RD",
|
'rd': "RD",
|
||||||
}
|
}
|
||||||
@@ -35,10 +47,12 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
|
|
||||||
class VRFFromCSVForm(forms.ModelForm):
|
class VRFFromCSVForm(forms.ModelForm):
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['name', 'rd', 'description']
|
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||||
|
|
||||||
|
|
||||||
class VRFImportForm(BulkImportForm, BootstrapMixin):
|
class VRFImportForm(BulkImportForm, BootstrapMixin):
|
||||||
@@ -47,11 +61,18 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
|
|
||||||
class VRFBulkEditForm(forms.Form, BootstrapMixin):
|
class VRFBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
class VRFBulkDeleteForm(ConfirmationForm):
|
def vrf_tenant_choices():
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vrfs'))
|
||||||
|
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
|
||||||
|
|
||||||
|
|
||||||
|
class VRFFilterForm(forms.Form, BootstrapMixin):
|
||||||
|
tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices,
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -66,10 +87,6 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class RIRBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=RIR.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Aggregates
|
# Aggregates
|
||||||
#
|
#
|
||||||
@@ -103,16 +120,12 @@ class AggregateBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
|
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
|
||||||
date_added = forms.DateField(required=False)
|
date_added = forms.DateField(required=False)
|
||||||
description = forms.CharField(max_length=50, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def aggregate_rir_choices():
|
def aggregate_rir_choices():
|
||||||
rir_choices = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
rir_choices = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||||
return [(r.slug, '{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
|
return [(r.slug, u'{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
|
||||||
|
|
||||||
|
|
||||||
class AggregateFilterForm(forms.Form, BootstrapMixin):
|
class AggregateFilterForm(forms.Form, BootstrapMixin):
|
||||||
@@ -132,10 +145,6 @@ class RoleForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class RoleBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Role.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Prefixes
|
# Prefixes
|
||||||
#
|
#
|
||||||
@@ -149,7 +158,7 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'description']
|
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'prefix': "IPv4 or IPv6 network",
|
'prefix': "IPv4 or IPv6 network",
|
||||||
'vrf': "VRF (if applicable)",
|
'vrf': "VRF (if applicable)",
|
||||||
@@ -190,15 +199,48 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
|
|||||||
class PrefixFromCSVForm(forms.ModelForm):
|
class PrefixFromCSVForm(forms.ModelForm):
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||||
error_messages={'invalid_choice': 'VRF not found.'})
|
error_messages={'invalid_choice': 'VRF not found.'})
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
error_messages={'invalid_choice': 'Site not found.'})
|
||||||
|
vlan_group_name = forms.CharField(required=False)
|
||||||
|
vlan_vid = forms.IntegerField(required=False)
|
||||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
|
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
|
||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid role.'})
|
error_messages={'invalid_choice': 'Invalid role.'})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['prefix', 'vrf', 'site', 'status_name', 'role', 'description']
|
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
|
||||||
|
'description']
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
super(PrefixFromCSVForm, self).clean()
|
||||||
|
|
||||||
|
site = self.cleaned_data.get('site')
|
||||||
|
vlan_group_name = self.cleaned_data.get('vlan_group_name')
|
||||||
|
vlan_vid = self.cleaned_data.get('vlan_vid')
|
||||||
|
|
||||||
|
# Validate VLAN
|
||||||
|
vlan_group = None
|
||||||
|
if vlan_group_name:
|
||||||
|
try:
|
||||||
|
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
|
||||||
|
except VLANGroup.DoesNotExist:
|
||||||
|
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
|
||||||
|
if vlan_vid and vlan_group:
|
||||||
|
try:
|
||||||
|
self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid)
|
||||||
|
except VLAN.DoesNotExist:
|
||||||
|
self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid))
|
||||||
|
elif vlan_vid and site:
|
||||||
|
try:
|
||||||
|
self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid)
|
||||||
|
except VLAN.MultipleObjectsReturned:
|
||||||
|
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
|
||||||
|
elif vlan_vid:
|
||||||
|
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
m = super(PrefixFromCSVForm, self).save(commit=False)
|
m = super(PrefixFromCSVForm, self).save(commit=False)
|
||||||
@@ -216,49 +258,54 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
class PrefixBulkEditForm(forms.Form, BootstrapMixin):
|
class PrefixBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
|
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
||||||
help_text="Select the VRF to assign, or check below to remove VRF assignment")
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
|
|
||||||
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
|
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
|
||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||||
description = forms.CharField(max_length=50, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def prefix_vrf_choices():
|
def prefix_vrf_choices():
|
||||||
vrf_choices = [('', 'All'), (0, 'Global')]
|
vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes'))
|
||||||
vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in vrf_choices]
|
||||||
return vrf_choices
|
|
||||||
|
|
||||||
|
def tenant_choices():
|
||||||
|
tenant_choices = Tenant.objects.all()
|
||||||
|
return [(t.slug, t.name) for t in tenant_choices]
|
||||||
|
|
||||||
|
|
||||||
def prefix_site_choices():
|
def prefix_site_choices():
|
||||||
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
|
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
|
||||||
return [(s.slug, '{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
|
return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
|
||||||
|
|
||||||
|
|
||||||
def prefix_status_choices():
|
def prefix_status_choices():
|
||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||||
status_counts[status['status']] = status['count']
|
status_counts[status['status']] = status['count']
|
||||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
def prefix_role_choices():
|
def prefix_role_choices():
|
||||||
role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
|
role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
|
||||||
return [(r.slug, '{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
|
return [(r.slug, u'{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
|
||||||
|
|
||||||
|
|
||||||
class PrefixFilterForm(forms.Form, BootstrapMixin):
|
class PrefixFilterForm(forms.Form, BootstrapMixin):
|
||||||
parent = forms.CharField(required=False, label='Search Within')
|
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||||
vrf = forms.ChoiceField(required=False, choices=prefix_vrf_choices, label='VRF')
|
'placeholder': 'Network',
|
||||||
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices)
|
}))
|
||||||
|
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
|
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
|
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
|
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
|
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
||||||
|
|
||||||
|
|
||||||
@@ -281,7 +328,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description']
|
fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'address': "IPv4 or IPv6 address and mask",
|
'address': "IPv4 or IPv6 address and mask",
|
||||||
'vrf': "VRF (if applicable)",
|
'vrf': "VRF (if applicable)",
|
||||||
@@ -330,6 +377,8 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
|||||||
class IPAddressFromCSVForm(forms.ModelForm):
|
class IPAddressFromCSVForm(forms.ModelForm):
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||||
error_messages={'invalid_choice': 'VRF not found.'})
|
error_messages={'invalid_choice': 'VRF not found.'})
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Device not found.'})
|
error_messages={'invalid_choice': 'Device not found.'})
|
||||||
interface_name = forms.CharField(required=False)
|
interface_name = forms.CharField(required=False)
|
||||||
@@ -337,7 +386,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description']
|
fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
@@ -368,9 +417,9 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
|||||||
name=self.cleaned_data['interface_name'])
|
name=self.cleaned_data['interface_name'])
|
||||||
# Set as primary for device
|
# Set as primary for device
|
||||||
if self.cleaned_data['is_primary']:
|
if self.cleaned_data['is_primary']:
|
||||||
if self.instance.family == 4:
|
if self.instance.address.version == 4:
|
||||||
self.instance.primary_ip4_for = self.cleaned_data['device']
|
self.instance.primary_ip4_for = self.cleaned_data['device']
|
||||||
elif self.instance.family == 6:
|
elif self.instance.address.version == 6:
|
||||||
self.instance.primary_ip6_for = self.cleaned_data['device']
|
self.instance.primary_ip6_for = self.cleaned_data['device']
|
||||||
|
|
||||||
return super(IPAddressFromCSVForm, self).save(commit=commit)
|
return super(IPAddressFromCSVForm, self).save(commit=commit)
|
||||||
@@ -382,14 +431,9 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
|
|
||||||
class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
|
class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
|
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
||||||
help_text="Select the VRF to assign, or check below to remove VRF assignment")
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
|
description = forms.CharField(max_length=100, required=False)
|
||||||
description = forms.CharField(max_length=50, required=False)
|
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def ipaddress_family_choices():
|
def ipaddress_family_choices():
|
||||||
@@ -397,14 +441,41 @@ def ipaddress_family_choices():
|
|||||||
|
|
||||||
|
|
||||||
def ipaddress_vrf_choices():
|
def ipaddress_vrf_choices():
|
||||||
vrf_choices = [('', 'All'), (0, 'Global')]
|
vrf_choices = VRF.objects.annotate(ipaddress_count=Count('ip_addresses'))
|
||||||
vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
return [(v.pk, u'{} ({})'.format(v.name, v.ipaddress_count)) for v in vrf_choices]
|
||||||
return vrf_choices
|
|
||||||
|
|
||||||
|
|
||||||
class IPAddressFilterForm(forms.Form, BootstrapMixin):
|
class IPAddressFilterForm(forms.Form, BootstrapMixin):
|
||||||
|
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||||
|
'placeholder': 'Prefix',
|
||||||
|
}))
|
||||||
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
|
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
|
||||||
vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF')
|
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
|
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# VLAN groups
|
||||||
|
#
|
||||||
|
|
||||||
|
class VLANGroupForm(forms.ModelForm, BootstrapMixin):
|
||||||
|
slug = SlugField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANGroup
|
||||||
|
fields = ['site', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
|
def vlangroup_site_choices():
|
||||||
|
site_choices = Site.objects.annotate(vlangroup_count=Count('vlan_groups'))
|
||||||
|
return [(s.slug, u'{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupFilterForm(forms.Form, BootstrapMixin):
|
||||||
|
site = forms.MultipleChoiceField(required=False, choices=vlangroup_site_choices,
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -412,29 +483,52 @@ class IPAddressFilterForm(forms.Form, BootstrapMixin):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANForm(forms.ModelForm, BootstrapMixin):
|
class VLANForm(forms.ModelForm, BootstrapMixin):
|
||||||
|
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
|
||||||
|
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
||||||
|
))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site', 'vid', 'name', 'status', 'role']
|
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which this VLAN exists",
|
'site': "The site at which this VLAN exists",
|
||||||
|
'group': "VLAN group (optional)",
|
||||||
'vid': "Configured VLAN ID",
|
'vid': "Configured VLAN ID",
|
||||||
'name': "Configured VLAN name",
|
'name': "Configured VLAN name",
|
||||||
'status': "Operational status of this VLAN",
|
'status': "Operational status of this VLAN",
|
||||||
'role': "The primary function of this VLAN",
|
'role': "The primary function of this VLAN",
|
||||||
}
|
}
|
||||||
|
widgets = {
|
||||||
|
'site': forms.Select(attrs={'filter-for': 'group'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
super(VLANForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Limit VLAN group choices
|
||||||
|
if self.is_bound and self.data.get('site'):
|
||||||
|
self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
|
||||||
|
elif self.initial.get('site'):
|
||||||
|
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
||||||
|
else:
|
||||||
|
self.fields['group'].choices = []
|
||||||
|
|
||||||
|
|
||||||
class VLANFromCSVForm(forms.ModelForm):
|
class VLANFromCSVForm(forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Device not found.'})
|
error_messages={'invalid_choice': 'Device not found.'})
|
||||||
|
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'VLAN group not found.'})
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
|
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
|
||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid role.'})
|
error_messages={'invalid_choice': 'Invalid role.'})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site', 'vid', 'name', 'status_name', 'role']
|
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
m = super(VLANFromCSVForm, self).save(commit=False)
|
m = super(VLANFromCSVForm, self).save(commit=False)
|
||||||
@@ -452,34 +546,47 @@ class VLANImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
class VLANBulkEditForm(forms.Form, BootstrapMixin):
|
class VLANBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||||
|
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
|
||||||
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
|
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
|
||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||||
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
class VLANBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def vlan_site_choices():
|
def vlan_site_choices():
|
||||||
site_choices = Site.objects.annotate(vlan_count=Count('vlans'))
|
site_choices = Site.objects.annotate(vlan_count=Count('vlans'))
|
||||||
return [(s.slug, '{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
|
return [(s.slug, u'{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
|
||||||
|
|
||||||
|
|
||||||
|
def vlan_group_choices():
|
||||||
|
group_choices = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
|
||||||
|
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
|
||||||
|
|
||||||
|
|
||||||
|
def vlan_tenant_choices():
|
||||||
|
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vlans'))
|
||||||
|
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
|
||||||
|
|
||||||
|
|
||||||
def vlan_status_choices():
|
def vlan_status_choices():
|
||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||||
status_counts[status['status']] = status['count']
|
status_counts[status['status']] = status['count']
|
||||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
def vlan_role_choices():
|
def vlan_role_choices():
|
||||||
role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
|
role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
|
||||||
return [(r.slug, '{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
|
return [(r.slug, u'{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
|
||||||
|
|
||||||
|
|
||||||
class VLANFilterForm(forms.Form, BootstrapMixin):
|
class VLANFilterForm(forms.Form, BootstrapMixin):
|
||||||
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
|
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices,
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
|
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
|
||||||
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
|
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||||
|
|||||||
20
netbox/ipam/migrations/0002_vrf_add_enforce_unique.py
Normal file
20
netbox/ipam/migrations/0002_vrf_add_enforce_unique.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-14 19:34
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vrf',
|
||||||
|
name='enforce_unique',
|
||||||
|
field=models.BooleanField(default=True, help_text=b'Prevent duplicate prefixes/IP addresses within this VRF', verbose_name=b'Enforce unique space'),
|
||||||
|
),
|
||||||
|
]
|
||||||
38
netbox/ipam/migrations/0003_ipam_add_vlangroups.py
Normal file
38
netbox/ipam/migrations/0003_ipam_add_vlangroups.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-15 16:22
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0010_devicebay_installed_device_set_null'),
|
||||||
|
('ipam', '0002_vrf_add_enforce_unique'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VLANGroup',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=50)),
|
||||||
|
('slug', models.SlugField()),
|
||||||
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vlan_groups', to='dcim.Site')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['site', 'name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='group',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='ipam.VLANGroup'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='vlangroup',
|
||||||
|
unique_together=set([('site', 'name'), ('site', 'slug')]),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
netbox/ipam/migrations/0004_ipam_vlangroup_uniqueness.py
Normal file
27
netbox/ipam/migrations/0004_ipam_vlangroup_uniqueness.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-15 17:14
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0003_ipam_add_vlangroups'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='vlan',
|
||||||
|
options={'ordering': ['site', 'group', 'vid'], 'verbose_name': 'VLAN', 'verbose_name_plural': 'VLANs'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='vlangroup',
|
||||||
|
options={'ordering': ['site', 'name'], 'verbose_name': 'VLAN group', 'verbose_name_plural': 'VLAN groups'},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='vlan',
|
||||||
|
unique_together=set([('group', 'name'), ('group', 'vid')]),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
netbox/ipam/migrations/0005_auto_20160725_1842.py
Normal file
25
netbox/ipam/migrations/0005_auto_20160725_1842.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-07-25 18:42
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0004_ipam_vlangroup_uniqueness'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=64),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py
Normal file
27
netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-07-27 14:39
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tenancy', '0001_initial'),
|
||||||
|
('ipam', '0005_auto_20160725_1842'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='tenant',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='tenancy.Tenant'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vrf',
|
||||||
|
name='tenant',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vrfs', to='tenancy.Tenant'),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py
Normal file
27
netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-07-28 15:32
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tenancy', '0001_initial'),
|
||||||
|
('ipam', '0006_vrf_vlan_add_tenant'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='tenant',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='tenancy.Tenant'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='tenant',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='tenancy.Tenant'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
from netaddr import IPNetwork, cidr_merge
|
from netaddr import IPNetwork, cidr_merge
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
|
from tenancy.models import Tenant
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
|
|
||||||
from .fields import IPNetworkField, IPAddressField
|
from .fields import IPNetworkField, IPAddressField
|
||||||
@@ -45,6 +47,9 @@ class VRF(CreatedUpdatedModel):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
|
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
|
||||||
|
tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
|
enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
|
||||||
|
help_text="Prevent duplicate prefixes/IP addresses within this VRF")
|
||||||
description = models.CharField(max_length=100, blank=True)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -62,6 +67,8 @@ class VRF(CreatedUpdatedModel):
|
|||||||
return ','.join([
|
return ','.join([
|
||||||
self.name,
|
self.name,
|
||||||
self.rd,
|
self.rd,
|
||||||
|
self.tenant.name if self.tenant else '',
|
||||||
|
'True' if self.enforce_unique else '',
|
||||||
self.description,
|
self.description,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -123,6 +130,8 @@ class Aggregate(CreatedUpdatedModel):
|
|||||||
|
|
||||||
# Ensure that the aggregate being added does not cover an existing aggregate
|
# Ensure that the aggregate being added does not cover an existing aggregate
|
||||||
covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix))
|
covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix))
|
||||||
|
if self.pk:
|
||||||
|
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
|
||||||
if covered_aggregates:
|
if covered_aggregates:
|
||||||
raise ValidationError("{} is overlaps with an existing aggregate ({})"
|
raise ValidationError("{} is overlaps with an existing aggregate ({})"
|
||||||
.format(self.prefix, covered_aggregates[0]))
|
.format(self.prefix, covered_aggregates[0]))
|
||||||
@@ -224,6 +233,7 @@ class Prefix(CreatedUpdatedModel):
|
|||||||
site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
|
site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
|
||||||
vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
|
vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
|
||||||
verbose_name='VRF')
|
verbose_name='VRF')
|
||||||
|
tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
|
vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
|
||||||
verbose_name='VLAN')
|
verbose_name='VLAN')
|
||||||
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
|
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
|
||||||
@@ -242,6 +252,15 @@ class Prefix(CreatedUpdatedModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:prefix', args=[self.pk])
|
return reverse('ipam:prefix', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
# Disallow host masks
|
||||||
|
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
|
||||||
|
raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses "
|
||||||
|
"instead.")
|
||||||
|
elif self.prefix.version == 6 and self.prefix.prefixlen == 128:
|
||||||
|
raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses "
|
||||||
|
"instead.")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.prefix:
|
if self.prefix:
|
||||||
# Clear host bits from prefix
|
# Clear host bits from prefix
|
||||||
@@ -277,7 +296,7 @@ class Prefix(CreatedUpdatedModel):
|
|||||||
|
|
||||||
class IPAddress(CreatedUpdatedModel):
|
class IPAddress(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
An IPAddress represents an individual IPV4 or IPv6 address and its mask. The mask length should match what is
|
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
|
||||||
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
|
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
|
||||||
Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface.
|
Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface.
|
||||||
Interfaces can have zero or more IPAddresses assigned to them.
|
Interfaces can have zero or more IPAddresses assigned to them.
|
||||||
@@ -290,6 +309,7 @@ class IPAddress(CreatedUpdatedModel):
|
|||||||
address = IPAddressField()
|
address = IPAddressField()
|
||||||
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
|
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
|
||||||
verbose_name='VRF')
|
verbose_name='VRF')
|
||||||
|
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
|
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
|
||||||
null=True)
|
null=True)
|
||||||
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
|
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
|
||||||
@@ -307,6 +327,21 @@ class IPAddress(CreatedUpdatedModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:ipaddress', args=[self.pk])
|
return reverse('ipam:ipaddress', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Enforce unique IP space if applicable
|
||||||
|
if self.vrf and self.vrf.enforce_unique:
|
||||||
|
duplicate_ips = IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip))\
|
||||||
|
.exclude(pk=self.pk)
|
||||||
|
if duplicate_ips:
|
||||||
|
raise ValidationError("Duplicate IP address found in VRF {}: {}".format(self.vrf,
|
||||||
|
duplicate_ips.first()))
|
||||||
|
elif not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE:
|
||||||
|
duplicate_ips = IPAddress.objects.filter(vrf=None, address__net_host=str(self.address.ip))\
|
||||||
|
.exclude(pk=self.pk)
|
||||||
|
if duplicate_ips:
|
||||||
|
raise ValidationError("Duplicate IP address found in global table: {}".format(duplicate_ips.first()))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.address:
|
if self.address:
|
||||||
# Infer address family from IPAddress object
|
# Infer address family from IPAddress object
|
||||||
@@ -338,23 +373,57 @@ class IPAddress(CreatedUpdatedModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroup(models.Model):
|
||||||
|
"""
|
||||||
|
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
||||||
|
"""
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
slug = models.SlugField()
|
||||||
|
site = models.ForeignKey('dcim.Site', related_name='vlan_groups')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['site', 'name']
|
||||||
|
unique_together = [
|
||||||
|
['site', 'name'],
|
||||||
|
['site', 'slug'],
|
||||||
|
]
|
||||||
|
verbose_name = 'VLAN group'
|
||||||
|
verbose_name_plural = 'VLAN groups'
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u'{} - {}'.format(self.site.name, self.name)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||||
|
|
||||||
|
|
||||||
class VLAN(CreatedUpdatedModel):
|
class VLAN(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
|
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
|
||||||
to a Site, however VLAN IDs need not be unique within a Site. Like Prefixes, each VLAN is assigned an operational
|
to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
|
||||||
status and optionally a user-defined Role. A VLAN can have zero or more Prefixes assigned to it.
|
within which all VLAN IDs and names but be unique.
|
||||||
|
|
||||||
|
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
|
||||||
|
or more Prefixes assigned to it.
|
||||||
"""
|
"""
|
||||||
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
|
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
|
||||||
|
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
||||||
MinValueValidator(1),
|
MinValueValidator(1),
|
||||||
MaxValueValidator(4094)
|
MaxValueValidator(4094)
|
||||||
])
|
])
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=64)
|
||||||
|
tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
|
status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
|
||||||
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
|
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
|
description = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['site', 'vid']
|
ordering = ['site', 'group', 'vid']
|
||||||
|
unique_together = [
|
||||||
|
['group', 'vid'],
|
||||||
|
['group', 'name'],
|
||||||
|
]
|
||||||
verbose_name = 'VLAN'
|
verbose_name = 'VLAN'
|
||||||
verbose_name_plural = 'VLANs'
|
verbose_name_plural = 'VLANs'
|
||||||
|
|
||||||
@@ -364,18 +433,27 @@ class VLAN(CreatedUpdatedModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:vlan', args=[self.pk])
|
return reverse('ipam:vlan', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Validate VLAN group
|
||||||
|
if self.group and self.group.site != self.site:
|
||||||
|
raise ValidationError("VLAN group must belong to the assigned site ({}).".format(self.site))
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return ','.join([
|
return ','.join([
|
||||||
self.site.name,
|
self.site.name,
|
||||||
|
self.group.name if self.group else '',
|
||||||
str(self.vid),
|
str(self.vid),
|
||||||
self.name,
|
self.name,
|
||||||
|
self.tenant.name if self.tenant else '',
|
||||||
self.get_status_display(),
|
self.get_status_display(),
|
||||||
self.role.name if self.role else '',
|
self.role.name if self.role else '',
|
||||||
|
self.description,
|
||||||
])
|
])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_name(self):
|
def display_name(self):
|
||||||
return u"{} ({})".format(self.vid, self.name)
|
return u'{} ({})'.format(self.vid, self.name)
|
||||||
|
|
||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return STATUS_CHOICE_CLASSES[self.status]
|
return STATUS_CHOICE_CLASSES[self.status]
|
||||||
|
|||||||
@@ -3,27 +3,24 @@ from django_tables2.utils import Accessor
|
|||||||
|
|
||||||
from utilities.tables import BaseTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
|
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
|
|
||||||
RIR_EDIT_LINK = """
|
RIR_ACTIONS = """
|
||||||
{% if perms.ipam.change_rir %}<a href="{% url 'ipam:rir_edit' slug=record.slug %}">Edit</a>{% endif %}
|
{% if perms.ipam.change_rir %}
|
||||||
|
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
UTILIZATION_GRAPH = """
|
UTILIZATION_GRAPH = """
|
||||||
{% with record.get_utilization as percentage %}
|
{% load helpers %}
|
||||||
<div class="progress text-center">
|
{% utilization_graph record.get_utilization %}
|
||||||
{% if percentage < 15 %}<span style="font-size: 12px;">{{ percentage }}%</span>{% endif %}
|
|
||||||
<div class="progress-bar progress-bar-{% if percentage >= 90 %}danger{% elif percentage >= 75 %}warning{% else %}success{% endif %}"
|
|
||||||
role="progressbar" aria-valuenow="{{ percentage }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage }}%">
|
|
||||||
{% if percentage >= 15 %}{{ percentage }}%{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endwith %}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROLE_EDIT_LINK = """
|
ROLE_ACTIONS = """
|
||||||
{% if perms.ipam.change_role %}<a href="{% url 'ipam:role_edit' slug=record.slug %}">Edit</a>{% endif %}
|
{% if perms.ipam.change_role %}
|
||||||
|
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PREFIX_LINK = """
|
PREFIX_LINK = """
|
||||||
@@ -42,6 +39,16 @@ PREFIX_LINK_BRIEF = """
|
|||||||
</span>
|
</span>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
IPADDRESS_LINK = """
|
||||||
|
{% if record.pk %}
|
||||||
|
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||||
|
{% elif perms.ipam.add_ipaddress %}
|
||||||
|
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ record.0 }}
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
STATUS_LABEL = """
|
STATUS_LABEL = """
|
||||||
{% if record.pk %}
|
{% if record.pk %}
|
||||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||||
@@ -50,6 +57,22 @@ STATUS_LABEL = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
VLANGROUP_ACTIONS = """
|
||||||
|
{% if perms.ipam.change_vlangroup %}
|
||||||
|
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
TENANT_LINK = """
|
||||||
|
{% if record.tenant %}
|
||||||
|
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}">{{ record.tenant }}</a>
|
||||||
|
{% elif record.vrf.tenant %}
|
||||||
|
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}">{{ record.vrf.tenant }}</a>*
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VRFs
|
# VRFs
|
||||||
@@ -59,11 +82,12 @@ class VRFTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
|
name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
|
||||||
rd = tables.Column(verbose_name='RD')
|
rd = tables.Column(verbose_name='RD')
|
||||||
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
description = tables.Column(orderable=False, verbose_name='Description')
|
description = tables.Column(orderable=False, verbose_name='Description')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ('pk', 'name', 'rd', 'description')
|
fields = ('pk', 'name', 'rd', 'tenant', 'description')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -75,11 +99,11 @@ class RIRTable(BaseTable):
|
|||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
aggregate_count = tables.Column(verbose_name='Aggregates')
|
aggregate_count = tables.Column(verbose_name='Aggregates')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
edit = tables.TemplateColumn(template_code=RIR_EDIT_LINK, verbose_name='')
|
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = RIR
|
model = RIR
|
||||||
fields = ('pk', 'name', 'aggregate_count', 'slug', 'edit')
|
fields = ('pk', 'name', 'aggregate_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -110,11 +134,11 @@ class RoleTable(BaseTable):
|
|||||||
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
||||||
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
edit = tables.TemplateColumn(template_code=ROLE_EDIT_LINK, verbose_name='')
|
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Role
|
model = Role
|
||||||
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'edit')
|
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -125,25 +149,31 @@ class PrefixTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||||
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
|
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
|
||||||
vrf = tables.Column(orderable=False, default='Global', verbose_name='VRF')
|
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
||||||
|
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
role = tables.Column(verbose_name='Role')
|
role = tables.Column(verbose_name='Role')
|
||||||
description = tables.Column(orderable=False, verbose_name='Description')
|
description = tables.Column(orderable=False, verbose_name='Description')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ('pk', 'prefix', 'status', 'vrf', 'site', 'role', 'description')
|
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
|
||||||
|
row_attrs = {
|
||||||
|
'class': lambda record: 'success' if not record.pk else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PrefixBriefTable(BaseTable):
|
class PrefixBriefTable(BaseTable):
|
||||||
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix')
|
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix')
|
||||||
|
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||||
role = tables.Column(verbose_name='Role')
|
role = tables.Column(verbose_name='Role')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ('prefix', 'status', 'site', 'role')
|
fields = ('prefix', 'vrf', 'status', 'site', 'role')
|
||||||
|
orderable = False
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -152,8 +182,9 @@ class PrefixBriefTable(BaseTable):
|
|||||||
|
|
||||||
class IPAddressTable(BaseTable):
|
class IPAddressTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
|
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||||
vrf = tables.Column(orderable=False, default='Global', verbose_name='VRF')
|
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
||||||
|
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
|
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
|
||||||
verbose_name='Device')
|
verbose_name='Device')
|
||||||
interface = tables.Column(orderable=False, verbose_name='Interface')
|
interface = tables.Column(orderable=False, verbose_name='Interface')
|
||||||
@@ -161,7 +192,10 @@ class IPAddressTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ('pk', 'address', 'vrf', 'device', 'interface', 'description')
|
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||||
|
row_attrs = {
|
||||||
|
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBriefTable(BaseTable):
|
class IPAddressBriefTable(BaseTable):
|
||||||
@@ -177,6 +211,24 @@ class IPAddressBriefTable(BaseTable):
|
|||||||
fields = ('address', 'device', 'interface', 'nat_inside')
|
fields = ('address', 'device', 'interface', 'nat_inside')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# VLAN groups
|
||||||
|
#
|
||||||
|
|
||||||
|
class VLANGroupTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
|
vlan_count = tables.Column(verbose_name='VLANs')
|
||||||
|
slug = tables.Column(verbose_name='Slug')
|
||||||
|
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name='')
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = VLANGroup
|
||||||
|
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
@@ -185,10 +237,12 @@ class VLANTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
name = tables.Column(verbose_name='Name')
|
name = tables.Column(verbose_name='Name')
|
||||||
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||||
role = tables.Column(verbose_name='Role')
|
role = tables.Column(verbose_name='Role')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ('pk', 'vid', 'site', 'name', 'status', 'role')
|
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role')
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ urlpatterns = [
|
|||||||
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||||
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||||
|
|
||||||
|
# VLAN groups
|
||||||
|
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||||
|
url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
|
||||||
|
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||||
|
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||||
|
|
||||||
# VLANs
|
# VLANs
|
||||||
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
|
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
|
||||||
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),
|
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from netaddr import IPSet
|
import netaddr
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Q
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
@@ -12,7 +12,7 @@ from utilities.views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
|
|
||||||
def add_available_prefixes(parent, prefix_list):
|
def add_available_prefixes(parent, prefix_list):
|
||||||
@@ -21,7 +21,7 @@ def add_available_prefixes(parent, prefix_list):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Find all unallocated space
|
# Find all unallocated space
|
||||||
available_prefixes = IPSet(parent) ^ IPSet([p.prefix for p in prefix_list])
|
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
|
||||||
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
||||||
|
|
||||||
# Concatenate and sort complete list of children
|
# Concatenate and sort complete list of children
|
||||||
@@ -31,13 +31,65 @@ def add_available_prefixes(parent, prefix_list):
|
|||||||
return prefix_list
|
return prefix_list
|
||||||
|
|
||||||
|
|
||||||
|
def add_available_ipaddresses(prefix, ipaddress_list):
|
||||||
|
"""
|
||||||
|
Annotate ranges of available IP addresses within a given prefix.
|
||||||
|
"""
|
||||||
|
|
||||||
|
output = []
|
||||||
|
prev_ip = None
|
||||||
|
|
||||||
|
# Ignore the "network address" for IPv4 prefixes larger than /31
|
||||||
|
if prefix.version == 4 and prefix.prefixlen < 31:
|
||||||
|
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
||||||
|
else:
|
||||||
|
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||||
|
|
||||||
|
# Ignore the broadcast address for IPv4 prefixes larger than /31
|
||||||
|
if prefix.version == 4 and prefix.prefixlen < 31:
|
||||||
|
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||||
|
else:
|
||||||
|
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||||
|
|
||||||
|
if not ipaddress_list:
|
||||||
|
return [(
|
||||||
|
int(last_ip_in_prefix - first_ip_in_prefix + 1),
|
||||||
|
'{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||||
|
)]
|
||||||
|
|
||||||
|
# Account for any available IPs before the first real IP
|
||||||
|
if ipaddress_list[0].address.ip > first_ip_in_prefix:
|
||||||
|
skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
|
||||||
|
first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||||
|
output.append((skipped_count, first_skipped))
|
||||||
|
|
||||||
|
# Iterate through existing IPs and annotate free ranges
|
||||||
|
for ip in ipaddress_list:
|
||||||
|
if prev_ip:
|
||||||
|
diff = int(ip.address.ip - prev_ip.address.ip)
|
||||||
|
if diff > 1:
|
||||||
|
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||||
|
output.append((diff - 1, first_skipped))
|
||||||
|
output.append(ip)
|
||||||
|
prev_ip = ip
|
||||||
|
|
||||||
|
# Include any remaining available IPs
|
||||||
|
if prev_ip.address.ip < last_ip_in_prefix:
|
||||||
|
skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
|
||||||
|
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||||
|
output.append((skipped_count, first_skipped))
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VRFListView(ObjectListView):
|
class VRFListView(ObjectListView):
|
||||||
queryset = VRF.objects.all()
|
queryset = VRF.objects.select_related('tenant')
|
||||||
filter = filters.VRFFilter
|
filter = filters.VRFFilter
|
||||||
|
filter_form = forms.VRFFilterForm
|
||||||
table = tables.VRFTable
|
table = tables.VRFTable
|
||||||
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
|
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
|
||||||
template_name = 'ipam/vrf_list.html'
|
template_name = 'ipam/vrf_list.html'
|
||||||
@@ -47,10 +99,11 @@ def vrf(request, pk):
|
|||||||
|
|
||||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||||
prefixes = Prefix.objects.filter(vrf=vrf)
|
prefixes = Prefix.objects.filter(vrf=vrf)
|
||||||
|
prefix_table = tables.PrefixBriefTable(prefixes)
|
||||||
|
|
||||||
return render(request, 'ipam/vrf.html', {
|
return render(request, 'ipam/vrf.html', {
|
||||||
'vrf': vrf,
|
'vrf': vrf,
|
||||||
'prefixes': prefixes,
|
'prefix_table': prefix_table,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -85,6 +138,10 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form):
|
||||||
|
|
||||||
fields_to_update = {}
|
fields_to_update = {}
|
||||||
|
if form.cleaned_data['tenant'] == 0:
|
||||||
|
fields_to_update['tenant'] = None
|
||||||
|
elif form.cleaned_data['tenant']:
|
||||||
|
fields_to_update['tenant'] = form.cleaned_data['tenant']
|
||||||
for field in ['description']:
|
for field in ['description']:
|
||||||
if form.cleaned_data[field]:
|
if form.cleaned_data[field]:
|
||||||
fields_to_update[field] = form.cleaned_data[field]
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
@@ -95,7 +152,6 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_vrf'
|
permission_required = 'ipam.delete_vrf'
|
||||||
cls = VRF
|
cls = VRF
|
||||||
form = forms.VRFBulkDeleteForm
|
|
||||||
default_redirect_url = 'ipam:vrf_list'
|
default_redirect_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -121,7 +177,6 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_rir'
|
permission_required = 'ipam.delete_rir'
|
||||||
cls = RIR
|
cls = RIR
|
||||||
form = forms.RIRBulkDeleteForm
|
|
||||||
default_redirect_url = 'ipam:rir_list'
|
default_redirect_url = 'ipam:rir_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -217,7 +272,6 @@ class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_aggregate'
|
permission_required = 'ipam.delete_aggregate'
|
||||||
cls = Aggregate
|
cls = Aggregate
|
||||||
form = forms.AggregateBulkDeleteForm
|
|
||||||
default_redirect_url = 'ipam:aggregate_list'
|
default_redirect_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -243,7 +297,6 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_role'
|
permission_required = 'ipam.delete_role'
|
||||||
cls = Role
|
cls = Role
|
||||||
form = forms.RoleBulkDeleteForm
|
|
||||||
default_redirect_url = 'ipam:role_list'
|
default_redirect_url = 'ipam:role_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -252,7 +305,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PrefixListView(ObjectListView):
|
class PrefixListView(ObjectListView):
|
||||||
queryset = Prefix.objects.select_related('site', 'role')
|
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'role')
|
||||||
filter = filters.PrefixFilter
|
filter = filters.PrefixFilter
|
||||||
filter_form = forms.PrefixFilterForm
|
filter_form = forms.PrefixFilterForm
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
@@ -275,10 +328,12 @@ def prefix(request, pk):
|
|||||||
aggregate = None
|
aggregate = None
|
||||||
|
|
||||||
# Count child IP addresses
|
# Count child IP addresses
|
||||||
ipaddress_count = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix)).count()
|
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
|
||||||
|
.count()
|
||||||
|
|
||||||
# Parent prefixes table
|
# Parent prefixes table
|
||||||
parent_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contains=str(prefix.prefix))\
|
parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
|
||||||
|
.filter(prefix__net_contains=str(prefix.prefix))\
|
||||||
.select_related('site', 'role').annotate_depth()
|
.select_related('site', 'role').annotate_depth()
|
||||||
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
||||||
|
|
||||||
@@ -288,7 +343,13 @@ def prefix(request, pk):
|
|||||||
duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes)
|
duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes)
|
||||||
|
|
||||||
# Child prefixes table
|
# Child prefixes table
|
||||||
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\
|
if prefix.vrf:
|
||||||
|
# If the prefix is in a VRF, show child prefixes only within that VRF.
|
||||||
|
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf)
|
||||||
|
else:
|
||||||
|
# If the prefix is in the global table, show child prefixes from all VRFs.
|
||||||
|
child_prefixes = Prefix.objects.all()
|
||||||
|
child_prefixes = child_prefixes.filter(prefix__net_contained=str(prefix.prefix))\
|
||||||
.select_related('site', 'role').annotate_depth(limit=0)
|
.select_related('site', 'role').annotate_depth(limit=0)
|
||||||
if child_prefixes:
|
if child_prefixes:
|
||||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||||
@@ -312,7 +373,7 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'ipam.change_prefix'
|
permission_required = 'ipam.change_prefix'
|
||||||
model = Prefix
|
model = Prefix
|
||||||
form_class = forms.PrefixForm
|
form_class = forms.PrefixForm
|
||||||
fields_initial = ['site', 'vrf', 'prefix']
|
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
|
||||||
cancel_url = 'ipam:prefix_list'
|
cancel_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -340,10 +401,11 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form):
|
||||||
|
|
||||||
fields_to_update = {}
|
fields_to_update = {}
|
||||||
if form.cleaned_data['vrf']:
|
for field in ['vrf', 'tenant']:
|
||||||
fields_to_update['vrf'] = form.cleaned_data['vrf']
|
if form.cleaned_data[field] == 0:
|
||||||
elif form.cleaned_data['vrf_global']:
|
fields_to_update[field] = None
|
||||||
fields_to_update['vrf'] = None
|
elif form.cleaned_data[field]:
|
||||||
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
for field in ['site', 'status', 'role', 'description']:
|
for field in ['site', 'status', 'role', 'description']:
|
||||||
if form.cleaned_data[field]:
|
if form.cleaned_data[field]:
|
||||||
fields_to_update[field] = form.cleaned_data[field]
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
@@ -354,7 +416,6 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_prefix'
|
permission_required = 'ipam.delete_prefix'
|
||||||
cls = Prefix
|
cls = Prefix
|
||||||
form = forms.PrefixBulkDeleteForm
|
|
||||||
default_redirect_url = 'ipam:prefix_list'
|
default_redirect_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -363,8 +424,9 @@ def prefix_ipaddresses(request, pk):
|
|||||||
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
||||||
|
|
||||||
# Find all IPAddresses belonging to this Prefix
|
# Find all IPAddresses belonging to this Prefix
|
||||||
ipaddresses = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix))\
|
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
|
||||||
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
||||||
|
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses)
|
||||||
|
|
||||||
ip_table = tables.IPAddressTable(ipaddresses)
|
ip_table = tables.IPAddressTable(ipaddresses)
|
||||||
ip_table.model = IPAddress
|
ip_table.model = IPAddress
|
||||||
@@ -383,7 +445,7 @@ def prefix_ipaddresses(request, pk):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class IPAddressListView(ObjectListView):
|
class IPAddressListView(ObjectListView):
|
||||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
queryset = IPAddress.objects.select_related('vrf__tenant', 'interface__device')
|
||||||
filter = filters.IPAddressFilter
|
filter = filters.IPAddressFilter
|
||||||
filter_form = forms.IPAddressFilterForm
|
filter_form = forms.IPAddressFilterForm
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
@@ -465,10 +527,11 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form):
|
||||||
|
|
||||||
fields_to_update = {}
|
fields_to_update = {}
|
||||||
if form.cleaned_data['vrf']:
|
for field in ['vrf', 'tenant']:
|
||||||
fields_to_update['vrf'] = form.cleaned_data['vrf']
|
if form.cleaned_data[field] == 0:
|
||||||
elif form.cleaned_data['vrf_global']:
|
fields_to_update[field] = None
|
||||||
fields_to_update['vrf'] = None
|
elif form.cleaned_data[field]:
|
||||||
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
for field in ['description']:
|
for field in ['description']:
|
||||||
if form.cleaned_data[field]:
|
if form.cleaned_data[field]:
|
||||||
fields_to_update[field] = form.cleaned_data[field]
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
@@ -479,10 +542,35 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_ipaddress'
|
permission_required = 'ipam.delete_ipaddress'
|
||||||
cls = IPAddress
|
cls = IPAddress
|
||||||
form = forms.IPAddressBulkDeleteForm
|
|
||||||
default_redirect_url = 'ipam:ipaddress_list'
|
default_redirect_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# VLAN groups
|
||||||
|
#
|
||||||
|
|
||||||
|
class VLANGroupListView(ObjectListView):
|
||||||
|
queryset = VLANGroup.objects.annotate(vlan_count=Count('vlans'))
|
||||||
|
filter = filters.VLANGroupFilter
|
||||||
|
filter_form = forms.VLANGroupFilterForm
|
||||||
|
table = tables.VLANGroupTable
|
||||||
|
edit_permissions = ['ipam.change_vlangroup', 'ipam.delete_vlangroup']
|
||||||
|
template_name = 'ipam/vlangroup_list.html'
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'ipam.change_vlangroup'
|
||||||
|
model = VLANGroup
|
||||||
|
form_class = forms.VLANGroupForm
|
||||||
|
cancel_url = 'ipam:vlangroup_list'
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'ipam.delete_vlangroup'
|
||||||
|
cls = VLANGroup
|
||||||
|
default_redirect_url = 'ipam:vlangroup_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
@@ -500,10 +588,11 @@ def vlan(request, pk):
|
|||||||
|
|
||||||
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
|
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
|
||||||
prefixes = Prefix.objects.filter(vlan=vlan)
|
prefixes = Prefix.objects.filter(vlan=vlan)
|
||||||
|
prefix_table = tables.PrefixBriefTable(prefixes)
|
||||||
|
|
||||||
return render(request, 'ipam/vlan.html', {
|
return render(request, 'ipam/vlan.html', {
|
||||||
'vlan': vlan,
|
'vlan': vlan,
|
||||||
'prefixes': prefixes,
|
'prefix_table': prefix_table,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -538,7 +627,11 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form):
|
||||||
|
|
||||||
fields_to_update = {}
|
fields_to_update = {}
|
||||||
for field in ['site', 'status', 'role']:
|
if form.cleaned_data['tenant'] == 0:
|
||||||
|
fields_to_update['tenant'] = None
|
||||||
|
elif form.cleaned_data['tenant']:
|
||||||
|
fields_to_update['tenant'] = form.cleaned_data['tenant']
|
||||||
|
for field in ['site', 'group', 'status', 'role', 'description']:
|
||||||
if form.cleaned_data[field]:
|
if form.cleaned_data[field]:
|
||||||
fields_to_update[field] = form.cleaned_data[field]
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
|
|
||||||
@@ -548,5 +641,4 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_vlan'
|
permission_required = 'ipam.delete_vlan'
|
||||||
cls = VLAN
|
cls = VLAN
|
||||||
form = forms.VLANBulkDeleteForm
|
|
||||||
default_redirect_url = 'ipam:vlan_list'
|
default_redirect_url = 'ipam:vlan_list'
|
||||||
|
|||||||
@@ -78,3 +78,11 @@ SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
|
|||||||
# banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
# banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||||
BANNER_TOP = ''
|
BANNER_TOP = ''
|
||||||
BANNER_BOTTOM = ''
|
BANNER_BOTTOM = ''
|
||||||
|
|
||||||
|
# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
|
||||||
|
# prefer IPv4 instead.
|
||||||
|
PREFER_IPV4 = False
|
||||||
|
|
||||||
|
# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table
|
||||||
|
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
|
||||||
|
ENFORCE_GLOBAL_UNIQUE = False
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.2.0'
|
VERSION = '1.4.2'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
@@ -40,6 +40,8 @@ DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
|||||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||||
|
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||||
|
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
|
||||||
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
||||||
|
|
||||||
# Attempt to import LDAP configuration if it has been defined
|
# Attempt to import LDAP configuration if it has been defined
|
||||||
@@ -106,6 +108,7 @@ INSTALLED_APPS = (
|
|||||||
'ipam',
|
'ipam',
|
||||||
'extras',
|
'extras',
|
||||||
'secrets',
|
'secrets',
|
||||||
|
'tenancy',
|
||||||
'users',
|
'users',
|
||||||
'utilities',
|
'utilities',
|
||||||
)
|
)
|
||||||
@@ -137,7 +140,6 @@ TEMPLATES = [
|
|||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
'utilities.context_processors.settings',
|
'utilities.context_processors.settings',
|
||||||
'django.core.context_processors.request',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ from django.conf.urls import include, url
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.views.defaults import page_not_found
|
from django.views.defaults import page_not_found
|
||||||
|
|
||||||
from views import home, trigger_500
|
from views import home, trigger_500, handle_500
|
||||||
from users.views import login, logout
|
from users.views import login, logout
|
||||||
|
|
||||||
|
|
||||||
|
handler500 = handle_500
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
# Default page
|
# Default page
|
||||||
@@ -20,6 +22,7 @@ urlpatterns = [
|
|||||||
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
|
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
|
||||||
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
|
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
|
||||||
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
|
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
|
||||||
|
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
|
||||||
url(r'^profile/', include('users.urls', namespace='users')),
|
url(r'^profile/', include('users.urls', namespace='users')),
|
||||||
|
|
||||||
# API
|
# API
|
||||||
@@ -27,6 +30,7 @@ urlpatterns = [
|
|||||||
url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
|
url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
|
||||||
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
|
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
|
||||||
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
|
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
|
||||||
|
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
|
||||||
url(r'^api/docs/', include('rest_framework_swagger.urls')),
|
url(r'^api/docs/', include('rest_framework_swagger.urls')),
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
from markdown import markdown
|
import sys
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.http import Http404
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
from circuits.models import Provider, Circuit
|
from circuits.models import Provider, Circuit
|
||||||
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
|
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
|
||||||
from extras.models import UserAction
|
from extras.models import UserAction
|
||||||
from ipam.models import Aggregate, Prefix, IPAddress, VLAN
|
from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
|
||||||
from secrets.models import Secret
|
from secrets.models import Secret
|
||||||
|
from tenancy.models import Tenant
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
|
|
||||||
# DCIM
|
# Organization
|
||||||
'site_count': Site.objects.count(),
|
'site_count': Site.objects.count(),
|
||||||
|
'tenant_count': Tenant.objects.count(),
|
||||||
|
|
||||||
|
# DCIM
|
||||||
'rack_count': Rack.objects.count(),
|
'rack_count': Rack.objects.count(),
|
||||||
'device_count': Device.objects.count(),
|
'device_count': Device.objects.count(),
|
||||||
'interface_connections_count': InterfaceConnection.objects.count(),
|
'interface_connections_count': InterfaceConnection.objects.count(),
|
||||||
@@ -25,6 +26,7 @@ def home(request):
|
|||||||
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
|
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
|
||||||
|
|
||||||
# IPAM
|
# IPAM
|
||||||
|
'vrf_count': VRF.objects.count(),
|
||||||
'aggregate_count': Aggregate.objects.count(),
|
'aggregate_count': Aggregate.objects.count(),
|
||||||
'prefix_count': Prefix.objects.count(),
|
'prefix_count': Prefix.objects.count(),
|
||||||
'ipaddress_count': IPAddress.objects.count(),
|
'ipaddress_count': IPAddress.objects.count(),
|
||||||
@@ -41,12 +43,20 @@ def home(request):
|
|||||||
|
|
||||||
return render(request, 'home.html', {
|
return render(request, 'home.html', {
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'recent_activity': UserAction.objects.select_related('user')[:15]
|
'recent_activity': UserAction.objects.select_related('user')[:50]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def trigger_500(request):
|
def trigger_500(request):
|
||||||
"""Hot-wired method of triggering a server error to test reporting."""
|
"""Hot-wired method of triggering a server error to test reporting."""
|
||||||
|
|
||||||
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
|
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
|
||||||
"person you are.")
|
"person you are.")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_500(request):
|
||||||
|
"""Custom server error handler"""
|
||||||
|
type_, error, traceback = sys.exc_info()
|
||||||
|
return render(request, '500.html', {
|
||||||
|
'exception': str(type_),
|
||||||
|
'error': error,
|
||||||
|
}, status=500)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
html {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -222,6 +225,22 @@ ul.rack li.h41u { height: 820px; }
|
|||||||
ul.rack li.h41u a, ul.rack li.h41u span { padding: 400px 0; }
|
ul.rack li.h41u a, ul.rack li.h41u span { padding: 400px 0; }
|
||||||
ul.rack li.h42u { height: 840px; }
|
ul.rack li.h42u { height: 840px; }
|
||||||
ul.rack li.h42u a, ul.rack li.h42u span { padding: 410px 0; }
|
ul.rack li.h42u a, ul.rack li.h42u span { padding: 410px 0; }
|
||||||
|
ul.rack li.h43u { height: 860px; }
|
||||||
|
ul.rack li.h43u a, ul.rack li.h43u span { padding: 420px 0; }
|
||||||
|
ul.rack li.h44u { height: 880px; }
|
||||||
|
ul.rack li.h44u a, ul.rack li.h44u span { padding: 430px 0; }
|
||||||
|
ul.rack li.h45u { height: 900px; }
|
||||||
|
ul.rack li.h45u a, ul.rack li.h45u span { padding: 440px 0; }
|
||||||
|
ul.rack li.h46u { height: 920px; }
|
||||||
|
ul.rack li.h46u a, ul.rack li.h46u span { padding: 450px 0; }
|
||||||
|
ul.rack li.h47u { height: 940px; }
|
||||||
|
ul.rack li.h47u a, ul.rack li.h47u span { padding: 460px 0; }
|
||||||
|
ul.rack li.h48u { height: 960px; }
|
||||||
|
ul.rack li.h48u a, ul.rack li.h48u span { padding: 470px 0; }
|
||||||
|
ul.rack li.h49u { height: 980px; }
|
||||||
|
ul.rack li.h49u a, ul.rack li.h49u span { padding: 480px 0; }
|
||||||
|
ul.rack li.h50u { height: 1000px; }
|
||||||
|
ul.rack li.h50u a, ul.rack li.h50u span { padding: 490px 0; }
|
||||||
ul.rack li.occupied a {
|
ul.rack li.occupied a {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
// "Select all" checkbox in a table header
|
// "Select all" checkbox in a table header
|
||||||
$('th input:checkbox').click(function (event) {
|
$('th input:checkbox[name=_all]').click(function (event) {
|
||||||
$(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked'));
|
$(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked'));
|
||||||
});
|
});
|
||||||
|
// Uncheck the "select all" checkbox if an item is unchecked
|
||||||
|
$('input:checkbox[name=pk]').click(function (event) {
|
||||||
|
if (!$(this).attr('checked')) {
|
||||||
|
$(this).parents('table').find('input:checkbox[name=_all]').prop('checked', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Slugify
|
// Slugify
|
||||||
function slugify(s, num_chars) {
|
function slugify(s, num_chars) {
|
||||||
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
||||||
s = s.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing spaces
|
s = s.replace(/^[\s\.]+|[\s\.]+$/g, ''); // Trim leading/trailing spaces
|
||||||
s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens
|
s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens
|
||||||
s = s.toLowerCase(); // Convert to lowercase
|
s = s.toLowerCase(); // Convert to lowercase
|
||||||
return s.substring(0, num_chars); // Trim to first num_chars chars
|
return s.substring(0, num_chars); // Trim to first num_chars chars
|
||||||
|
|||||||
@@ -10,15 +10,16 @@ $(document).ready(function() {
|
|||||||
$('#privkey_modal').modal('show');
|
$('#privkey_modal').modal('show');
|
||||||
} else {
|
} else {
|
||||||
unlock_secret(secret_id, private_key);
|
unlock_secret(secret_id, private_key);
|
||||||
$(this).hide();
|
|
||||||
$(this).siblings('button.lock-secret').show();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Locking a secret
|
// Locking a secret
|
||||||
$('button.lock-secret').click(function (event) {
|
$('button.lock-secret').click(function (event) {
|
||||||
var secret_id = $(this).attr('secret-id');
|
var secret_id = $(this).attr('secret-id');
|
||||||
$('#secret_' + secret_id).html('********');
|
var secret_div = $('#secret_' + secret_id);
|
||||||
|
|
||||||
|
// Delete the plaintext
|
||||||
|
secret_div.html('********');
|
||||||
$(this).hide();
|
$(this).hide();
|
||||||
$(this).siblings('button.unlock-secret').show();
|
$(this).siblings('button.unlock-secret').show();
|
||||||
});
|
});
|
||||||
@@ -81,13 +82,16 @@ $(document).ready(function() {
|
|||||||
xhr.setRequestHeader("X-CSRFToken", csrf_token);
|
xhr.setRequestHeader("X-CSRFToken", csrf_token);
|
||||||
},
|
},
|
||||||
success: function (response, status) {
|
success: function (response, status) {
|
||||||
var secret_plaintext = response.plaintext;
|
$('#secret_' + secret_id).html(response.plaintext);
|
||||||
$('#secret_' + secret_id).html(secret_plaintext);
|
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
|
||||||
return true;
|
$('button.lock-secret[secret-id=' + secret_id + ']').show();
|
||||||
},
|
},
|
||||||
error: function (xhr, ajaxOptions, thrownError) {
|
error: function (xhr, ajaxOptions, thrownError) {
|
||||||
if (xhr.status == 403) {
|
if (xhr.status == 403) {
|
||||||
alert("Decryption failed: " + xhr.statusText);
|
alert("Permission denied");
|
||||||
|
} else {
|
||||||
|
var json = jQuery.parseJSON(xhr.responseText);
|
||||||
|
alert("Decryption failed: " + json['error']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class SecretRoleListView(generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
queryset = SecretRole.objects.all()
|
queryset = SecretRole.objects.all()
|
||||||
serializer_class = serializers.SecretRoleSerializer
|
serializer_class = serializers.SecretRoleSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleDetailView(generics.RetrieveAPIView):
|
class SecretRoleDetailView(generics.RetrieveAPIView):
|
||||||
@@ -36,17 +37,19 @@ class SecretRoleDetailView(generics.RetrieveAPIView):
|
|||||||
"""
|
"""
|
||||||
queryset = SecretRole.objects.all()
|
queryset = SecretRole.objects.all()
|
||||||
serializer_class = serializers.SecretRoleSerializer
|
serializer_class = serializers.SecretRoleSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
class SecretListView(generics.GenericAPIView):
|
class SecretListView(generics.GenericAPIView):
|
||||||
"""
|
"""
|
||||||
List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret.
|
List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret.
|
||||||
"""
|
"""
|
||||||
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
|
queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
|
||||||
.prefetch_related('role__users', 'role__groups')
|
.prefetch_related('role__users', 'role__groups')
|
||||||
serializer_class = serializers.SecretSerializer
|
serializer_class = serializers.SecretSerializer
|
||||||
filter_class = SecretFilter
|
filter_class = SecretFilter
|
||||||
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, private_key=None):
|
def get(self, request, private_key=None):
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
@@ -87,10 +90,11 @@ class SecretDetailView(generics.GenericAPIView):
|
|||||||
"""
|
"""
|
||||||
Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret.
|
Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret.
|
||||||
"""
|
"""
|
||||||
queryset = Secret.objects.select_related('device__primary_ip', 'role')\
|
queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
|
||||||
.prefetch_related('role__users', 'role__groups')
|
.prefetch_related('role__users', 'role__groups')
|
||||||
serializer_class = serializers.SecretSerializer
|
serializer_class = serializers.SecretSerializer
|
||||||
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get(self, request, pk, private_key=None):
|
def get(self, request, pk, private_key=None):
|
||||||
secret = get_object_or_404(Secret, pk=pk)
|
secret = get_object_or_404(Secret, pk=pk)
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from .models import Secret, SecretRole
|
from .models import Secret, SecretRole
|
||||||
|
from dcim.models import Device
|
||||||
|
|
||||||
|
|
||||||
class SecretFilter(django_filters.FilterSet):
|
class SecretFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.MethodFilter(
|
||||||
|
action='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role',
|
||||||
queryset=SecretRole.objects.all(),
|
queryset=SecretRole.objects.all(),
|
||||||
@@ -15,7 +22,19 @@ class SecretFilter(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
|
device = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='device',
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label='Device (Name)',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ['name', 'role_id', 'role']
|
fields = ['name', 'role_id', 'role', 'device']
|
||||||
|
|
||||||
|
def search(self, queryset, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(device__name__icontains=value)
|
||||||
|
)
|
||||||
|
|||||||
42
netbox/secrets/fixtures/initial_data.json
Normal file
42
netbox/secrets/fixtures/initial_data.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "secrets.secretrole",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "Login Credentials",
|
||||||
|
"slug": "login-credentials",
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "secrets.secretrole",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "RADIUS Key",
|
||||||
|
"slug": "radius-key",
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "secrets.secretrole",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "SNMPv2 Community",
|
||||||
|
"slug": "snmpv2-community",
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "secrets.secretrole",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "SNMPv3 Credentials",
|
||||||
|
"slug": "snmpv3-credentials",
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -5,7 +5,7 @@ from django import forms
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.forms import BootstrapMixin, BulkImportForm, ConfirmationForm, CSVDataField, SlugField
|
from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, SlugField
|
||||||
|
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
|
|
||||||
@@ -42,10 +42,6 @@ class SecretRoleForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=SecretRole.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Secrets
|
# Secrets
|
||||||
#
|
#
|
||||||
@@ -97,13 +93,9 @@ class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
name = forms.CharField(max_length=100, required=False)
|
name = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkDeleteForm(ConfirmationForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
def secret_role_choices():
|
def secret_role_choices():
|
||||||
role_choices = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
role_choices = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
||||||
return [(r.slug, '{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
|
return [(r.slug, u'{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
|
||||||
|
|
||||||
|
|
||||||
class SecretFilterForm(forms.Form, BootstrapMixin):
|
class SecretFilterForm(forms.Form, BootstrapMixin):
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ class Secret(CreatedUpdatedModel):
|
|||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.role and self.device:
|
if self.role and self.device:
|
||||||
return "{} for {}".format(self.role, self.device)
|
return u'{} for {}'.format(self.role, self.device)
|
||||||
return "Secret"
|
return u'Secret'
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('secrets:secret', args=[self.pk])
|
return reverse('secrets:secret', args=[self.pk])
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
|
|||||||
from .models import SecretRole, Secret
|
from .models import SecretRole, Secret
|
||||||
|
|
||||||
|
|
||||||
SECRETROLE_EDIT_LINK = """
|
SECRETROLE_ACTIONS = """
|
||||||
{% if perms.secrets.change_secretrole %}
|
{% if perms.secrets.change_secretrole %}
|
||||||
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}">Edit</a>
|
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -22,11 +22,12 @@ class SecretRoleTable(BaseTable):
|
|||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
secret_count = tables.Column(verbose_name='Secrets')
|
secret_count = tables.Column(verbose_name='Secrets')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
edit = tables.TemplateColumn(template_code=SECRETROLE_EDIT_LINK, verbose_name='')
|
actions = tables.TemplateColumn(template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name='')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
fields = ('pk', 'name', 'secret_count', 'slug', 'edit')
|
fields = ('pk', 'name', 'secret_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'secrets.delete_secretrole'
|
permission_required = 'secrets.delete_secretrole'
|
||||||
cls = SecretRole
|
cls = SecretRole
|
||||||
form = forms.SecretRoleBulkDeleteForm
|
|
||||||
default_redirect_url = 'secrets:secretrole_list'
|
default_redirect_url = 'secrets:secretrole_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ def secret_add(request, pk):
|
|||||||
|
|
||||||
messages.success(request, "Added new secret: {0}".format(secret))
|
messages.success(request, "Added new secret: {0}".format(secret))
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
return redirect('secrets:secret_add')
|
return redirect('dcim:device_addsecret', pk=device.pk)
|
||||||
else:
|
else:
|
||||||
return redirect('secrets:secret', pk=secret.pk)
|
return redirect('secrets:secret', pk=secret.pk)
|
||||||
|
|
||||||
@@ -219,5 +218,4 @@ class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'secrets.delete_secret'
|
permission_required = 'secrets.delete_secret'
|
||||||
cls = Secret
|
cls = Secret
|
||||||
form = forms.SecretBulkDeleteForm
|
|
||||||
default_redirect_url = 'secrets:secret_list'
|
default_redirect_url = 'secrets:secret_list'
|
||||||
|
|||||||
@@ -12,13 +12,19 @@
|
|||||||
<div class="col-md-4 col-md-offset-4">
|
<div class="col-md-4 col-md-offset-4">
|
||||||
<div class="panel panel-danger" style="margin-top: 200px">
|
<div class="panel panel-danger" style="margin-top: 200px">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Server Error</strong>
|
<strong>
|
||||||
|
<i class="fa fa-warning"></i>
|
||||||
|
Server Error
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p>There was a problem with your request. This error has been logged and administrative staff have
|
<p>There was a problem with your request. This error has been logged and administrative staff have
|
||||||
been notified. Please return to the home page and try again.</p>
|
been notified. Please return to the home page and try again.</p>
|
||||||
<p>If you are responsible for this installation, please consider
|
<p>If you are responsible for this installation, please consider
|
||||||
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>.</p>
|
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>. Additional
|
||||||
|
information is provided below:</p>
|
||||||
|
<pre><strong>{{ exception }}</strong><br />
|
||||||
|
{{ error }}</pre>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a href="/" class="btn btn-primary">Home Page</a>
|
<a href="/" class="btn btn-primary">Home Page</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,170 +24,182 @@
|
|||||||
<div id="navbar" class="navbar-collapse collapse">
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' or 'tenancy' in request.path %} active{% endif %}">
|
||||||
{% if perms.dcim.add_site %}
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Sites <span class="caret"></span></a>
|
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'dcim:site_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Sites</a></li>
|
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
|
||||||
<li><a href="{% url 'dcim:site_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Site</a></li>
|
{% if perms.dcim.add_site %}
|
||||||
<li><a href="{% url 'dcim:site_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Sites</a></li>
|
<li><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Site</a></li>
|
||||||
</ul>
|
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'dcim:site_list' %}">Sites</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
|
||||||
|
{% if perms.tenancy.add_tenant %}
|
||||||
|
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>
|
||||||
|
<li><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Tenants</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenant Groups</a></li>
|
||||||
|
{% if perms.tenancy.add_tenantgroup %}
|
||||||
|
<li><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant Group</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/dcim/rack' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/dcim/rack' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'dcim:rack_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Racks</a></li>
|
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
|
||||||
{% if perms.dcim.add_rack %}
|
{% if perms.dcim.add_rack %}
|
||||||
<li><a href="{% url 'dcim:rack_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack</a></li>
|
<li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li>
|
||||||
<li><a href="{% url 'dcim:rack_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Racks</a></li>
|
<li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Rack Groups</a></li>
|
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Groups</a></li>
|
||||||
{% if perms.dcim.add_rackgroup %}
|
{% if perms.dcim.add_rackgroup %}
|
||||||
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
|
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'dcim:device_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Devices</a></li>
|
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
|
||||||
{% if perms.dcim.add_device %}
|
{% if perms.dcim.add_device %}
|
||||||
<li><a href="{% url 'dcim:device_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device</a></li>
|
<li><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device</a></li>
|
||||||
<li><a href="{% url 'dcim:device_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Devices</a></li>
|
<li><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Devices</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_device or perms.ipam.add_devicetype %}
|
{% if perms.ipam.add_device or perms.ipam.add_devicetype %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Device Types</a></li>
|
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Types</a></li>
|
||||||
{% if perms.dcim.add_devicetype %}
|
{% if perms.dcim.add_devicetype %}
|
||||||
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Type</a></li>
|
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Type</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Device Roles</a></li>
|
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Roles</a></li>
|
||||||
{% if perms.dcim.add_devicerole %}
|
{% if perms.dcim.add_devicerole %}
|
||||||
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Role</a></li>
|
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Role</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
|
{% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Manufacturers</a></li>
|
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Manufacturers</a></li>
|
||||||
{% if perms.dcim.add_manufacturer %}
|
{% if perms.dcim.add_manufacturer %}
|
||||||
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
|
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
|
{% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'dcim:platform_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Platforms</a></li>
|
<li><a href="{% url 'dcim:platform_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Platforms</a></li>
|
||||||
{% if perms.dcim.add_platform %}
|
{% if perms.dcim.add_platform %}
|
||||||
<li><a href="{% url 'dcim:platform_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Platform</a></li>
|
<li><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Platform</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/dcim/console-connections/' or request.path|startswith:'/dcim/power-connections/' or request.path|startswith:'/dcim/interface-connections/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/dcim/console-connections/' or request.path|startswith:'/dcim/power-connections/' or request.path|startswith:'/dcim/interface-connections/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Console Connections</a></li>
|
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
|
||||||
{% if perms.dcim.change_consoleport %}
|
{% if perms.dcim.change_consoleport %}
|
||||||
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Console Connections</a></li>
|
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Console Connections</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
|
{% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Power Connections</a></li>
|
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Power Connections</a></li>
|
||||||
{% if perms.dcim.change_powerport %}
|
{% if perms.dcim.change_powerport %}
|
||||||
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Power Connections</a></li>
|
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Power Connections</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
|
{% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Interface Connections</a></li>
|
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Interface Connections</a></li>
|
||||||
{% if perms.dcim.add_interfaceconnection %}
|
{% if perms.dcim.add_interfaceconnection %}
|
||||||
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Interface Connections</a></li>
|
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Interface Connections</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlans/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlan' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> IP Addresses</a></li>
|
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
|
||||||
{% if perms.ipam.add_ipaddress %}
|
{% if perms.ipam.add_ipaddress %}
|
||||||
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an IP</a></li>
|
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an IP</a></li>
|
||||||
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import IPs</a></li>
|
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import IPs</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
|
{% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'ipam:prefix_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Prefixes</a></li>
|
<li><a href="{% url 'ipam:prefix_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefixes</a></li>
|
||||||
{% if perms.ipam.add_prefix %}
|
{% if perms.ipam.add_prefix %}
|
||||||
<li><a href="{% url 'ipam:prefix_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Prefix</a></li>
|
<li><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Prefix</a></li>
|
||||||
<li><a href="{% url 'ipam:prefix_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Prefixes</a></li>
|
<li><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Prefixes</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
|
{% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Aggregates</a></li>
|
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Aggregates</a></li>
|
||||||
{% if perms.ipam.add_aggregate %}
|
{% if perms.ipam.add_aggregate %}
|
||||||
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
|
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
|
||||||
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Aggregates</a></li>
|
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Aggregates</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
|
{% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'ipam:vrf_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VRFs</a></li>
|
<li><a href="{% url 'ipam:vrf_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VRFs</a></li>
|
||||||
{% if perms.ipam.add_vrf %}
|
{% if perms.ipam.add_vrf %}
|
||||||
<li><a href="{% url 'ipam:vrf_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VRF</a></li>
|
<li><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VRF</a></li>
|
||||||
<li><a href="{% url 'ipam:vrf_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VRFs</a></li>
|
<li><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VRFs</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li><a href="{% url 'ipam:rir_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> RIRs</a></li>
|
<li><a href="{% url 'ipam:rir_list' %}"><i class="fa fa-search" aria-hidden="true"></i> RIRs</a></li>
|
||||||
{% if perms.ipam.add_rir %}
|
{% if perms.ipam.add_rir %}
|
||||||
<li><a href="{% url 'ipam:rir_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a RIR</a></li>
|
<li><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a RIR</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_rir or perms.ipam.add_role %}
|
{% if perms.ipam.add_rir or perms.ipam.add_role %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'ipam:role_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
|
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
|
||||||
{% if perms.ipam.add_role %}
|
{% if perms.ipam.add_role %}
|
||||||
<li><a href="{% url 'ipam:role_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Role</a></li>
|
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/ipam/vlans/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/ipam/vlan' %} active{% endif %}">
|
||||||
{% if perms.ipam.add_vlan %}
|
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li>
|
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
|
||||||
<li><a href="{% url 'ipam:vlan_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN</a></li>
|
{% if perms.ipam.add_vlan %}
|
||||||
<li><a href="{% url 'ipam:vlan_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VLANs</a></li>
|
<li><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN</a></li>
|
||||||
</ul>
|
<li><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VLANs</a></li>
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'ipam:vlan_list' %}">VLANs</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Groups</a></li>
|
||||||
|
{% if perms.ipam.add_vlangroup %}
|
||||||
|
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'circuits:provider_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Providers</a></li>
|
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
|
||||||
{% if perms.circuits.add_provider %}
|
{% if perms.circuits.add_provider %}
|
||||||
<li><a href="{% url 'circuits:provider_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Provider</a></li>
|
<li><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Provider</a></li>
|
||||||
<li><a href="{% url 'circuits:provider_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Providers</a></li>
|
<li><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Providers</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.circuits.add_circuit or perms.circuits.add_provider %}
|
{% if perms.circuits.add_circuit or perms.circuits.add_provider %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'circuits:circuit_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Circuits</a></li>
|
<li><a href="{% url 'circuits:circuit_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuits</a></li>
|
||||||
{% if perms.circuits.add_circuit %}
|
{% if perms.circuits.add_circuit %}
|
||||||
<li><a href="{% url 'circuits:circuit_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit</a></li>
|
<li><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit</a></li>
|
||||||
<li><a href="{% url 'circuits:circuit_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Circuits</a></li>
|
<li><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Circuits</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Circuit Types</a></li>
|
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuit Types</a></li>
|
||||||
{% if perms.circuits.add_circuittype %}
|
{% if perms.circuits.add_circuittype %}
|
||||||
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
|
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -195,14 +207,14 @@
|
|||||||
<li class="dropdown{% if request.path|startswith:'/secrets/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/secrets/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'secrets:secret_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Secrets</a></li>
|
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
|
||||||
{% if perms.secrets.add_secret %}
|
{% if perms.secrets.add_secret %}
|
||||||
<li><a href="{% url 'secrets:secret_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Secrets</a></li>
|
<li><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Secrets</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Secret Roles</a></li>
|
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secret Roles</a></li>
|
||||||
{% if perms.secrets.add_secretrole %}
|
{% if perms.secrets.add_secretrole %}
|
||||||
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
|
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -212,12 +224,12 @@
|
|||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<li><a href="{% url 'admin:index' %}"><i class="glyphicon glyphicon-cog" aria-hidden="true"></i> Admin</a></li>
|
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'users:profile' %}"><i class="glyphicon glyphicon-user" aria-hidden="true"></i> Profile</a></li>
|
<li><a href="{% url 'users:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
|
||||||
<li><a href="{% url 'logout' %}"><i class="glyphicon glyphicon-log-out" aria-hidden="true"></i> Log out</a></li>
|
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="glyphicon glyphicon-log-in" aria-hidden="true"></i> Log in</a></li>
|
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% block title %}{{ circuit.provider }} Circuit {{ circuit.cid }}{% endblock %}
|
{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a></li>
|
<li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
|
||||||
<li><a href="{% url 'circuits:circuit_list' %}?site={{ circuit.site.slug }}">Circuits</a></li>
|
<li><a href="{% url 'circuits:circuit_list' %}?provider={{ circuit.provider.slug }}">{{ circuit.provider }}</a></li>
|
||||||
<li>{{ circuit }}</li>
|
<li>{{ circuit.cid }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Circuit ID" />
|
<input type="text" name="q" class="form-control" />
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
<span class="fa fa-search" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,18 +28,18 @@
|
|||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.circuits.change_circuit %}
|
{% if perms.circuits.change_circuit %}
|
||||||
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
|
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||||
Edit this circuit
|
Edit this circuit
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.circuits.delete_circuit %}
|
{% if perms.circuits.delete_circuit %}
|
||||||
<a href="{% url 'circuits:circuit_delete' pk=circuit.pk %}" class="btn btn-danger">
|
<a href="{% url 'circuits:circuit_delete' pk=circuit.pk %}" class="btn btn-danger">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||||
Delete this circuit
|
Delete this circuit
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ circuit.provider }} Circuit {{ circuit.cid }}</h1>
|
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -57,6 +57,67 @@
|
|||||||
<td>Circuit ID</td>
|
<td>Circuit ID</td>
|
||||||
<td>{{ circuit.cid }}</td>
|
<td>{{ circuit.cid }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Type</td>
|
||||||
|
<td><a href="{{ circuit.type.get_absolute_url }}">{{ circuit.type }}</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tenant</td>
|
||||||
|
<td>
|
||||||
|
{% if circuit.tenant %}
|
||||||
|
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Install Date</td>
|
||||||
|
<td>
|
||||||
|
{% if circuit.install_date %}
|
||||||
|
{{ circuit.install_date }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Port Speed</td>
|
||||||
|
<td>
|
||||||
|
{% if circuit.port_speed %}
|
||||||
|
{{ circuit.port_speed_human }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Commit Rate</td>
|
||||||
|
<td>
|
||||||
|
{% if circuit.commit_speed %}
|
||||||
|
{{ circuit.commit_speed_human }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Created</td>
|
||||||
|
<td>{{ circuit.created }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Last Updated</td>
|
||||||
|
<td>{{ circuit.last_updated }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Termination</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover panel-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Site</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -73,38 +134,28 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>Install Date</td>
|
|
||||||
<td>{{ circuit.install_date }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Port Speed</td>
|
|
||||||
<td>{{ circuit.port_speed_human }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Commit Rate</td>
|
|
||||||
<td>{% if circuit.commit_rate %}{{ circuit.commit_rate_human }}{% else %}<span class="text-muted">N/A</span>{% endif %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Cross-Connect</td>
|
<td>Cross-Connect</td>
|
||||||
<td>{{ circuit.xconnect_id }}</td>
|
<td>
|
||||||
|
{% if circuit.xconnect_id %}
|
||||||
|
{{ circuit.xconnect_id }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Patch Panel/Port</td>
|
<td>Patch Panel/Port</td>
|
||||||
<td>{{ circuit.pp_info }}</td>
|
<td>
|
||||||
</tr>
|
{% if circuit.pp_info %}
|
||||||
<tr>
|
{{ circuit.pp_info }}
|
||||||
<td>Created</td>
|
{% else %}
|
||||||
<td>{{ circuit.created }}</td>
|
<span class="text-muted">N/A</span>
|
||||||
</tr>
|
{% endif %}
|
||||||
<tr>
|
</td>
|
||||||
<td>Last Updated</td>
|
|
||||||
<td>{{ circuit.last_updated }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
|
|||||||
@@ -9,13 +9,19 @@
|
|||||||
{% render_field form.provider %}
|
{% render_field form.provider %}
|
||||||
{% render_field form.cid %}
|
{% render_field form.cid %}
|
||||||
{% render_field form.type %}
|
{% render_field form.type %}
|
||||||
|
{% render_field form.tenant %}
|
||||||
{% render_field form.install_date %}
|
{% render_field form.install_date %}
|
||||||
{% render_field form.port_speed %}
|
|
||||||
{% render_field form.commit_rate %}
|
|
||||||
{% render_field form.xconnect_id %}
|
{% render_field form.xconnect_id %}
|
||||||
{% render_field form.pp_info %}
|
{% render_field form.pp_info %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Bandwidth</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.port_speed %}
|
||||||
|
{% render_field form.commit_rate %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Termination</strong></div>
|
<div class="panel-heading"><strong>Termination</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|||||||
@@ -43,6 +43,11 @@
|
|||||||
<td>Circuit type</td>
|
<td>Circuit type</td>
|
||||||
<td>Transit</td>
|
<td>Transit</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tenant</td>
|
||||||
|
<td>Name of tenant (optional)</td>
|
||||||
|
<td>Strickland Propane</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Site</td>
|
||||||
<td>Site name</td>
|
<td>Site name</td>
|
||||||
@@ -76,7 +81,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<h4>Example</h4>
|
||||||
<pre>IC-603122,TeliaSonera,Transit,ASH-4,2016-02-23,10000,2000,937649,PP8371 ports 13/14</pre>
|
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,ASH-4,2016-02-23,10000,2000,937649,PP8371 ports 13/14</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.circuits.add_circuit %}
|
{% if perms.circuits.add_circuit %}
|
||||||
<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary">
|
<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a circuit
|
Add a circuit
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -19,23 +19,7 @@
|
|||||||
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
|
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="panel panel-default">
|
{% include 'inc/search_panel.html' %}
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Search</strong>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% include 'inc/filter_panel.html' %}
|
{% include 'inc/filter_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.circuits.add_circuittype %}
|
{% if perms.circuits.add_circuittype %}
|
||||||
<a href="{% url 'circuits:circuittype_add' %}" class="btn btn-primary">
|
<a href="{% url 'circuits:circuittype_add' %}" class="btn btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a circuit type
|
Add a circuit type
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -6,27 +6,41 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
|
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
|
||||||
<li>{{ provider }}</li>
|
<li>{{ provider }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<form action="{% url 'circuits:provider_list' %}" method="get">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="q" class="form-control" />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="fa fa-search" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
|
{% if show_graphs %}
|
||||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
|
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
|
||||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
<i class="fa fa-signal" aria-hidden="true"></i>
|
||||||
Graphs
|
Graphs
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% if perms.circuits.change_provider %}
|
{% if perms.circuits.change_provider %}
|
||||||
<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
|
<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||||
Edit this provider
|
Edit this provider
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.circuits.delete_provider %}
|
{% if perms.circuits.delete_provider %}
|
||||||
<a href="{% url 'circuits:provider_delete' slug=provider.slug %}" class="btn btn-danger">
|
<a href="{% url 'circuits:provider_delete' slug=provider.slug %}" class="btn btn-danger">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||||
Delete this provider
|
Delete this provider
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -41,25 +55,53 @@
|
|||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td>ASN</td>
|
<td>ASN</td>
|
||||||
<td>{{ provider.asn }}</td>
|
<td>
|
||||||
|
{% if provider.asn %}
|
||||||
|
{{ provider.asn }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Account</td>
|
<td>Account</td>
|
||||||
<td>{{ provider.account }}</td>
|
<td>
|
||||||
|
{% if provider.account %}
|
||||||
|
{{ provider.account }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Customer Portal</td>
|
<td>Customer Portal</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if provider.portal_url %}
|
||||||
<a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
|
<a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>NOC Contact</td>
|
<td>NOC Contact</td>
|
||||||
<td>{{ provider.noc_contact|linebreaksbr }}</td>
|
<td>
|
||||||
|
{% if provider.noc_contact %}
|
||||||
|
{{ provider.noc_contact|linebreaksbr }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Admin Contact</td>
|
<td>Admin Contact</td>
|
||||||
<td>{{ provider.admin_contact|linebreaksbr }}</td>
|
<td>
|
||||||
|
{% if provider.admin_contact %}
|
||||||
|
{{ provider.admin_contact|linebreaksbr }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Created</td>
|
<td>Created</td>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.circuits.add_provider %}
|
{% if perms.circuits.add_provider %}
|
||||||
<a href="{% url 'circuits:provider_add' %}" class="btn btn-primary">
|
<a href="{% url 'circuits:provider_add' %}" class="btn btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a provider
|
Add a provider
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -14,8 +14,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1>Providers</h1>
|
<h1>Providers</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-9">
|
||||||
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
|
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/filter_panel.html' %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.dcim.change_consoleport %}
|
{% if perms.dcim.change_consoleport %}
|
||||||
<a href="{% url 'dcim:console_connections_import' %}" class="btn btn-info">
|
<a href="{% url 'dcim:console_connections_import' %}" class="btn btn-info">
|
||||||
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
|
<span class="fa fa-download" aria-hidden="true"></span>
|
||||||
Import connections
|
Import connections
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -14,6 +14,16 @@
|
|||||||
<strong>Device</strong>
|
<strong>Device</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
|
<tr>
|
||||||
|
<td>Tenant</td>
|
||||||
|
<td>
|
||||||
|
{% if device.tenant %}
|
||||||
|
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Site</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -55,7 +65,7 @@
|
|||||||
{% if device.serial %}
|
{% if device.serial %}
|
||||||
<span>{{ device.serial }}</span>
|
<span>{{ device.serial }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not defined</span>
|
<span class="text-muted">N/A</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -86,7 +96,7 @@
|
|||||||
{% if device.platform %}
|
{% if device.platform %}
|
||||||
<span>{{ device.platform }}</span>
|
<span>{{ device.platform }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-warning">Not assigned</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -111,7 +121,7 @@
|
|||||||
<span>(NAT: {{ device.primary_ip4.nat_outside.address.ip }})</span>
|
<span>(NAT: {{ device.primary_ip4.nat_outside.address.ip }})</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not defined</span>
|
<span class="text-muted">N/A</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -126,7 +136,7 @@
|
|||||||
<span>(NAT: {{ device.primary_ip6.nat_outside.address.ip }})</span>
|
<span>(NAT: {{ device.primary_ip6.nat_outside.address.ip }})</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not defined</span>
|
<span class="text-muted">N/A</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -289,100 +299,180 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% if device_bays or device.device_type.is_parent_device %}
|
{% if device_bays or device.device_type.is_parent_device %}
|
||||||
|
{% if perms.dcim.delete_devicebay %}
|
||||||
|
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Device Bays</strong>
|
<strong>Device Bays</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for devicebay in device_bays %}
|
{% for devicebay in device_bays %}
|
||||||
{% include 'dcim/inc/_devicebay.html' %}
|
{% include 'dcim/inc/_devicebay.html' with selectable=True %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">No device bays defined</td>
|
<td colspan="4">No device bays defined</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% if perms.dcim.add_devicebay or perms.dcim.delete_devicebay %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if device_bays and perms.dcim.delete_devicebay %}
|
||||||
|
<button type="submit" class="btn btn-xs btn-danger">
|
||||||
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-right">
|
||||||
{% if perms.dcim.add_devicebay %}
|
{% if perms.dcim.add_devicebay %}
|
||||||
<div class="panel-footer text-right">
|
|
||||||
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
Add device bays
|
Add device bay
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if perms.dcim.delete_devicebay %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces or device.device_type.is_network_device %}
|
{% if interfaces or device.device_type.is_network_device %}
|
||||||
|
{% if perms.dcim.delete_interface %}
|
||||||
|
<form method="post" action="{% url 'dcim:interface_bulk_delete' pk=device.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Interfaces</strong>
|
<strong>Interfaces</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for iface in interfaces %}
|
{% for iface in interfaces %}
|
||||||
{% include 'dcim/inc/_interface.html' %}
|
{% include 'dcim/inc/_interface.html' with selectable=True %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">No interfaces defined</td>
|
<td colspan="4">No interfaces defined</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% if perms.dcim.add_interface or perms.dcim.delete_interface %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if interfaces and perms.dcim.delete_interface %}
|
||||||
|
<button type="submit" class="btn btn-xs btn-danger">
|
||||||
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-right">
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.dcim.add_interface %}
|
||||||
<div class="panel-footer text-right">
|
|
||||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
Add interface
|
Add interface
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if perms.dcim.delete_interface %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if cs_ports or device.device_type.is_console_server %}
|
{% if cs_ports or device.device_type.is_console_server %}
|
||||||
|
{% if perms.dcim.delete_consoleserverport %}
|
||||||
|
<form method="post" action="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Console Server Ports</strong>
|
<strong>Console Server Ports</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for csp in cs_ports %}
|
{% for csp in cs_ports %}
|
||||||
{% include 'dcim/inc/_consoleserverport.html' %}
|
{% include 'dcim/inc/_consoleserverport.html' with selectable=True %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">No console server ports defined</td>
|
<td colspan="4">No console server ports defined</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if cs_ports and perms.dcim.delete_consoleserverport %}
|
||||||
|
<button type="submit" class="btn btn-xs btn-danger">
|
||||||
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-right">
|
||||||
{% if perms.dcim.add_consoleserverport %}
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
<div class="panel-footer text-right">
|
|
||||||
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
Add console server ports
|
Add console server ports
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if perms.dcim.delete_consoleserverport %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if power_outlets or device.device_type.is_pdu %}
|
{% if power_outlets or device.device_type.is_pdu %}
|
||||||
|
{% if perms.dcim.delete_poweroutlet %}
|
||||||
|
<form method="post" action="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Power Outlets</strong>
|
<strong>Power Outlets</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for po in power_outlets %}
|
{% for po in power_outlets %}
|
||||||
{% include 'dcim/inc/_poweroutlet.html' %}
|
{% include 'dcim/inc/_poweroutlet.html' with selectable=True %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">No power outlets defined</td>
|
<td colspan="4">No power outlets defined</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if power_outlets and perms.dcim.delete_poweroutlet %}
|
||||||
|
<button type="submit" class="btn btn-xs btn-danger">
|
||||||
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-right">
|
||||||
{% if perms.dcim.add_poweroutlet %}
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
<div class="panel-footer text-right">
|
|
||||||
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
Add power outlets
|
Add power outlets
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if perms.dcim.delete_poweroutlet %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||||
<td>{{ device.device_type }}</td>
|
<td>{{ device.device_type }}</td>
|
||||||
<td>{{ device.device_role }}</td>
|
<td>{{ device.device_role }}</td>
|
||||||
|
<td>{{ device.tenant }}</td>
|
||||||
<td>{{ device.serial }}</td>
|
<td>{{ device.serial }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.device_role %}
|
{% render_field form.device_role %}
|
||||||
|
{% render_field form.tenant %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -22,8 +23,32 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
|
{% if obj.device_type.is_child_device and obj.parent_bay %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">Parent device</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">
|
||||||
|
<a href="{% url 'dcim:device' pk=obj.parent_bay.device.pk %}">{{ obj.parent_bay.device }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">Parent bay</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">
|
||||||
|
{{ obj.parent_bay.name }}
|
||||||
|
{% if perms.dcim.change_devicebay %}
|
||||||
|
<a href="{% url 'dcim:devicebay_depopulate' pk=obj.parent_bay.pk %}" class="btn btn-danger btn-xs">
|
||||||
|
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Remove device"></i> Remove
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif not obj.device_type.is_child_device %}
|
||||||
{% render_field form.face %}
|
{% render_field form.face %}
|
||||||
{% render_field form.position %}
|
{% render_field form.position %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block title %}Device Import{% endblock %}
|
{% block title %}Device Import{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Device Import</h1>
|
{% include 'dcim/inc/_device_import_header.html' %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<form action="." method="post" class="form">
|
<form action="." method="post" class="form">
|
||||||
@@ -36,6 +36,11 @@
|
|||||||
<td>Functional role of device</td>
|
<td>Functional role of device</td>
|
||||||
<td>ToR Switch</td>
|
<td>ToR Switch</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tenant</td>
|
||||||
|
<td>Name of tenant (optional)</td>
|
||||||
|
<td>Pied Piper</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Device manufacturer</td>
|
<td>Device manufacturer</td>
|
||||||
<td>Hardware manufacturer</td>
|
<td>Hardware manufacturer</td>
|
||||||
@@ -79,7 +84,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<h4>Example</h4>
|
||||||
<pre>rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
|
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
75
netbox/templates/dcim/device_import_child.html
Normal file
75
netbox/templates/dcim/device_import_child.html
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block title %}Device Import{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'dcim/inc/_device_import_header.html' with active_tab='child_import' %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<form action="." method="post" class="form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% render_form form %}
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<h4>CSV Format</h4>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Example</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>Device name (optional)</td>
|
||||||
|
<td>Blade12</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Device role</td>
|
||||||
|
<td>Functional role of device</td>
|
||||||
|
<td>Blade Server</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Device manufacturer</td>
|
||||||
|
<td>Hardware manufacturer</td>
|
||||||
|
<td>Dell</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Device model</td>
|
||||||
|
<td>Hardware model</td>
|
||||||
|
<td>BS2000T</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Platform</td>
|
||||||
|
<td>Software running on device (optional)</td>
|
||||||
|
<td>Linux</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Serial</td>
|
||||||
|
<td>Serial number (optional)</td>
|
||||||
|
<td>CAB00577291</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Parent device</td>
|
||||||
|
<td>Parent device</td>
|
||||||
|
<td>Server101</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Device bay</td>
|
||||||
|
<td>Device bay name</td>
|
||||||
|
<td>Slot 4</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h4>Example</h4>
|
||||||
|
<pre>Blade12,Blade Server,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if perms.dcim.add_module %}
|
{% if perms.dcim.add_module %}
|
||||||
<a href="{% url 'dcim:module_add' pk=device.pk %}" class="btn btn-success">
|
<a href="{% url 'dcim:module_add' pk=device.pk %}" class="btn btn-success">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a Module
|
Add a Module
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.dcim.add_device %}
|
{% if perms.dcim.add_device %}
|
||||||
<a href="{% url 'dcim:device_add' %}" class="btn btn-primary">
|
<a href="{% url 'dcim:device_add' %}" class="btn btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a device
|
Add a device
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'dcim:device_import' %}" class="btn btn-info">
|
<a href="{% url 'dcim:device_import' %}" class="btn btn-info">
|
||||||
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
|
<span class="fa fa-download" aria-hidden="true"></span>
|
||||||
Import devices
|
Import devices
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -23,23 +23,7 @@
|
|||||||
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
|
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="panel panel-default">
|
{% include 'inc/search_panel.html' %}
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Search</strong>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" name="q" class="form-control" placeholder="Name or serial" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% include 'inc/filter_panel.html' %}
|
{% include 'inc/filter_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.dcim.add_devicerole %}
|
{% if perms.dcim.add_devicerole %}
|
||||||
<a href="{% url 'dcim:devicerole_add' %}" class="btn btn-primary">
|
<a href="{% url 'dcim:devicerole_add' %}" class="btn btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a device role
|
Add a device role
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.dcim.change_devicetype %}
|
{% if perms.dcim.change_devicetype %}
|
||||||
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
|
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||||
Edit this device type
|
Edit this device type
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_devicetype %}
|
{% if perms.dcim.delete_devicetype %}
|
||||||
<a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
|
<a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||||
Delete this device type
|
Delete this device type
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -42,19 +42,35 @@
|
|||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Manufacturer</td>
|
<td>Manufacturer</td>
|
||||||
<td>{{ devicetype.manufacturer }}</td>
|
<td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Model Name</td>
|
<td>Model Name</td>
|
||||||
<td>{{ devicetype.model }}</td>
|
<td>{{ devicetype.model }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Part Number</td>
|
||||||
|
<td>
|
||||||
|
{% if devicetype.part_number %}
|
||||||
|
{{ devicetype.part_number }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Height (U)</td>
|
<td>Height (U)</td>
|
||||||
<td>{{ devicetype.u_height }}</td>
|
<td>{{ devicetype.u_height }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Full Depth</td>
|
<td>Full Depth</td>
|
||||||
<td>{{ devicetype.is_full_depth|yesno|capfirst }}</td>
|
<td>
|
||||||
|
{% if devicetype.is_full_depth %}
|
||||||
|
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,21 +80,70 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Is a Console Server</td>
|
<td class="text-right">
|
||||||
<td>{{ devicetype.is_console_server|yesno|capfirst }}</td>
|
{% if devicetype.is_console_server %}
|
||||||
|
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>Console Server</strong><br />
|
||||||
|
<small class="text-muted">This device {% if devicetype.is_console_server %}has{% else %}does not have{% endif %} console server ports</small>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Is a PDU</td>
|
<td class="text-right">
|
||||||
<td>{{ devicetype.is_pdu|yesno|capfirst }}</td>
|
{% if devicetype.is_pdu %}
|
||||||
|
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>PDU</strong><br />
|
||||||
|
<small class="text-muted">This device {% if devicetype.is_pdu %}has{% else %}does not have{% endif %} power outlets</small>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Is a Network Device</td>
|
<td class="text-right">
|
||||||
<td>{{ devicetype.is_network_device|yesno|capfirst }}</td>
|
{% if devicetype.is_network_device %}
|
||||||
|
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>Network Device</strong><br />
|
||||||
|
<small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} non-management network interfaces</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-right">
|
||||||
|
{% if devicetype.subdevice_role == True %}
|
||||||
|
<label class="label label-primary">Parent</label>
|
||||||
|
{% elif devicetype.subdevice_role == False %}
|
||||||
|
<label class="label label-info">Child</label>
|
||||||
|
{% else %}
|
||||||
|
<label class="label label-default">None</label>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>Parent/Child</strong><br />
|
||||||
|
{% if devicetype.subdevice_role == True %}
|
||||||
|
<small class="text-muted">This device has device bays for mounting child devices</small>
|
||||||
|
{% elif devicetype.subdevice_role == False %}
|
||||||
|
<small class="text-muted">This device can only be mounted in a parent device</small>
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted">This device does not have device bays</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
|
||||||
|
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' delete_url='dcim:devicetype_delete_interface' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% if devicetype.is_parent_device %}
|
{% if devicetype.is_parent_device %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.dcim.add_devicetype %}
|
{% if perms.dcim.add_devicetype %}
|
||||||
<a href="{% url 'dcim:devicetype_add' %}" class="btn btn-primary">
|
<a href="{% url 'dcim:devicetype_add' %}" class="btn btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a device type
|
Add a device type
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
<tr{% if cp.cs_port and not cp.connection_status %} class="info"{% endif %}>
|
<tr{% if cp.cs_port and not cp.connection_status %} class="info"{% endif %}>
|
||||||
|
{% if selectable and perms.dcim.delete_consoleport %}
|
||||||
|
<td class="pk">
|
||||||
|
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
<tr{% if csp.connected_console and not csp.connected_console.connection_status %} class="info"{% endif %}>
|
<tr{% if csp.connected_console and not csp.connected_console.connection_status %} class="info"{% endif %}>
|
||||||
|
{% if selectable and perms.dcim.delete_consoleserverport %}
|
||||||
|
<td class="pk">
|
||||||
|
<input name="pk" type="checkbox" value="{{ csp.pk }}" />
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }}
|
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Device name or serial" />
|
<input type="text" name="q" class="form-control" placeholder="Search devices" />
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
<span class="fa fa-search" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
5
netbox/templates/dcim/inc/_device_import_header.html
Normal file
5
netbox/templates/dcim/inc/_device_import_header.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<h1>Device Import</h1>
|
||||||
|
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||||
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}><a href="{% url 'dcim:device_import' %}">Racked Devices</a></li>
|
||||||
|
<li role="presentation"{% if active_tab == 'child_import' %} class="active"{% endif %}><a href="{% url 'dcim:device_import_child' %}">Child Devices</a></li>
|
||||||
|
</ul>
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
|
{% if selectable and perms.dcim.delete_devicebay %}
|
||||||
|
<td class="pk">
|
||||||
|
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
|
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
<tr{% if iface.connection and not iface.connection.connection_status %} class="info"{% endif %}>
|
<tr{% if iface.connection and not iface.connection.connection_status %} class="info"{% endif %}>
|
||||||
|
{% if selectable and perms.dcim.delete_interface %}
|
||||||
|
<td class="pk">
|
||||||
|
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
||||||
{% if iface.description %}
|
{% if iface.description %}
|
||||||
@@ -29,11 +34,13 @@
|
|||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
|
{% if show_graphs %}
|
||||||
{% if iface.circuit or iface.connection %}
|
{% if iface.circuit or iface.connection %}
|
||||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
|
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
|
||||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% if perms.dcim.change_interface %}
|
{% if perms.dcim.change_interface %}
|
||||||
{% if iface.is_physical %}
|
{% if iface.is_physical %}
|
||||||
{% if iface.connection %}
|
{% if iface.connection %}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
<tr{% if po.connected_port and not po.connected_port.connection_status %} class="info"{% endif %}>
|
<tr{% if po.connected_port and not po.connected_port.connection_status %} class="info"{% endif %}>
|
||||||
|
{% if selectable and perms.dcim.delete_poweroutlet %}
|
||||||
|
<td class="pk">
|
||||||
|
<input name="pk" type="checkbox" value="{{ po.pk }}" />
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-bolt"></i> {{ po.name }}
|
<i class="fa fa-fw fa-bolt"></i> {{ po.name }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user