mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-07 12:36:55 -06:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce26b566a4 | ||
|
|
ee2d0b963d | ||
|
|
f051c0e564 | ||
|
|
03d3bbcddb | ||
|
|
06cafb09b3 | ||
|
|
4670929953 | ||
|
|
366e2e7a94 | ||
|
|
0e14bc1e02 | ||
|
|
e5f05ca9be | ||
|
|
d08522408a | ||
|
|
cc31c8fc33 | ||
|
|
db60e8868c | ||
|
|
1adae67dd7 | ||
|
|
5ad3044314 | ||
|
|
9313ba08ed | ||
|
|
f3b9930dea | ||
|
|
5520144ff4 | ||
|
|
2b9ea58c86 | ||
|
|
c6970e1998 | ||
|
|
c61bae3a33 | ||
|
|
b0f9035e2d | ||
|
|
aba9748ffd | ||
|
|
2876ef7607 | ||
|
|
7d1aeede1a | ||
|
|
b7f4a11eee | ||
|
|
0e5138d6ec | ||
|
|
4d26fc7e7c | ||
|
|
102cf52a16 | ||
|
|
198ed859ff | ||
|
|
9d44d5d4e7 | ||
|
|
e8896fe238 | ||
|
|
181539651f | ||
|
|
b69564f5c9 | ||
|
|
c26e00b5bd | ||
|
|
dc606645fd | ||
|
|
1e1dd8c668 | ||
|
|
ce6796ed9b | ||
|
|
585e08eb95 | ||
|
|
d817990283 | ||
|
|
9905099a71 | ||
|
|
0eba5a0de3 | ||
|
|
5eb3c1a67b | ||
|
|
b370375414 | ||
|
|
8536f6c163 | ||
|
|
f4f41a5985 | ||
|
|
af3c9eaec1 | ||
|
|
c91f41e984 | ||
|
|
b8b2ea7ccb | ||
|
|
c90cecc2fb | ||
|
|
b2ef7bb104 | ||
|
|
5d5d4ac714 | ||
|
|
b3b96e5e10 | ||
|
|
6be520a8f9 | ||
|
|
f3db914e9d | ||
|
|
fbfa3cf619 | ||
|
|
1317c0dd8c | ||
|
|
bbc633b004 | ||
|
|
ed8fdd9292 | ||
|
|
2d9c33c34f | ||
|
|
80439c495e | ||
|
|
1bddd038fe | ||
|
|
d36923e47d | ||
|
|
476cbf17f6 | ||
|
|
91d50b9627 | ||
|
|
52420945b2 | ||
|
|
b70eca7661 | ||
|
|
39d083eae7 | ||
|
|
3bfc1ebcea | ||
|
|
b6bbcb0609 | ||
|
|
6121f97ca9 | ||
|
|
74e48fc490 | ||
|
|
28a9307f9f | ||
|
|
cdccc3a47f | ||
|
|
3eb969de0c | ||
|
|
9ff59ab686 | ||
|
|
fc7f88d2a2 | ||
|
|
769537fe98 | ||
|
|
f8a4f1b24f | ||
|
|
7f3b358571 | ||
|
|
c264281530 | ||
|
|
b3f20aa233 | ||
|
|
07997b24ca | ||
|
|
03859d7287 | ||
|
|
0ad2670822 | ||
|
|
ab706d2440 | ||
|
|
398faf518c | ||
|
|
edf29e7b9b | ||
|
|
485a21f13e | ||
|
|
eedec192ba | ||
|
|
cfaf8b9157 | ||
|
|
98e2145b52 | ||
|
|
466c505bb8 | ||
|
|
97c0f23c67 | ||
|
|
424c2a59d6 | ||
|
|
c9e7c12463 | ||
|
|
2ef1e623a3 | ||
|
|
1486a8901a | ||
|
|
73ae87aa57 | ||
|
|
ac72e90dcc | ||
|
|
dbf9840b26 | ||
|
|
09fe328c3f | ||
|
|
381eb664cf | ||
|
|
23c6451524 | ||
|
|
99cd78cbbf |
28
.github/ISSUE_TEMPLATE.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!--
|
||||||
|
Please note: GitHub issues are to be used only for feature requests
|
||||||
|
and bug reports. For installation assistance or general discussion,
|
||||||
|
please join us on the mailing list:
|
||||||
|
|
||||||
|
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||||
|
|
||||||
|
Please indicate "bug report" or "feature request" below. Be sure to
|
||||||
|
search the existing set of issues (both open and closed) to see if
|
||||||
|
a similar issue has already been raised.
|
||||||
|
-->
|
||||||
|
### Issue type:
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If filing a bug, please indicate the version of Python and NetBox
|
||||||
|
you are running. (This is not necessary for feature requests.)
|
||||||
|
-->
|
||||||
|
**Python version:**
|
||||||
|
**NetBox version:**
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If filing a bug, please record the exact steps taken to reproduce
|
||||||
|
the bug and any errors messages that are generated.
|
||||||
|
|
||||||
|
If filing a feature request, please precisely describe the data
|
||||||
|
model or workflow you would like to see implemented, and provide a
|
||||||
|
use case.
|
||||||
|
-->
|
||||||
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!--
|
||||||
|
Thank you for your interest in contributing to NetBox! Please note
|
||||||
|
that our contribution policy requires that a feature request or bug
|
||||||
|
report be opened for approval prior to filing a pull request. This
|
||||||
|
helps avoid wasting time and effort on something that we might not
|
||||||
|
be able to accept.
|
||||||
|
|
||||||
|
Please indicate the relevant feature request or bug report below.
|
||||||
|
-->
|
||||||
|
### Fixes:
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please include a summary of the proposed changes below.
|
||||||
|
-->
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,8 +1,10 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
/netbox/netbox/configuration.py
|
/netbox/netbox/configuration.py
|
||||||
|
/netbox/netbox/ldap_config.py
|
||||||
/netbox/static
|
/netbox/static
|
||||||
.idea
|
.idea
|
||||||
/*.sh
|
/*.sh
|
||||||
!upgrade.sh
|
!upgrade.sh
|
||||||
fabfile.py
|
fabfile.py
|
||||||
*.swp
|
*.swp
|
||||||
|
gunicorn_config.py
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ env:
|
|||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "2.7"
|
- "2.7"
|
||||||
|
- "3.4"
|
||||||
|
- "3.5"
|
||||||
|
- "3.6"
|
||||||
install:
|
install:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install pep8
|
- pip install pep8
|
||||||
|
|||||||
120
CONTRIBUTING.md
120
CONTRIBUTING.md
@@ -1,85 +1,113 @@
|
|||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
If you encounter any issues installing or using NetBox, try one of the following resources to get assistance. Please
|
If you encounter any issues installing or using NetBox, try one of the
|
||||||
**do not** open an issue on GitHub except to report bugs or request features.
|
following resources to get assistance. Please **do not** open a GitHub
|
||||||
|
issue except to report bugs or request features.
|
||||||
|
|
||||||
|
### Mailing List
|
||||||
|
|
||||||
|
We have established a Google Groups Mailing List for issues and general
|
||||||
|
discussion. This is the best forum for obtaining assistance with NetBox
|
||||||
|
installation. You can find us [here](https://groups.google.com/forum/#!forum/netbox-discuss).
|
||||||
|
|
||||||
### Freenode IRC
|
### Freenode IRC
|
||||||
|
|
||||||
Join the #netbox channel on [Freenode IRC](https://freenode.net/). You can connect to Freenode at irc.freenode.net using
|
For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/).
|
||||||
an IRC client, or you can use their [webchat client](https://webchat.freenode.net/).
|
You can connect to Freenode at irc.freenode.net using an IRC client, or
|
||||||
|
you can use their [webchat client](https://webchat.freenode.net/).
|
||||||
### Reddit
|
|
||||||
|
|
||||||
We have established [/r/netbox](https://www.reddit.com/r/netbox) on Reddit for NetBox issues and general discussion.
|
|
||||||
Reddit registration is free and does not require providing an email address (although it is encouraged).
|
|
||||||
|
|
||||||
## Reporting Bugs
|
## Reporting Bugs
|
||||||
|
|
||||||
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
|
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
|
||||||
NetBox. If you're running an older version, it's possible that the bug has already been fixed.
|
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
|
* 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
|
already been reported. If you think you may be experiencing a reported
|
||||||
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
|
issue that hasn't already been resolved, please click "add a reaction"
|
||||||
comment describing how it's affecting your installation. This will allow us to prioritize bugs based on how many users
|
in the top right corner of the issue and add a thumbs up (+1). You might
|
||||||
are affected.
|
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.
|
* If you haven't found an existing issue that describes your suspected
|
||||||
**Do not** file an issue until you have received confirmation that it is in fact a bug. Invalid issues are very
|
bug, please inquire about it on the mailing list. **Do not** file an
|
||||||
distracting and slow the pace at which NetBox is developed.
|
issue until you have received confirmation that it is in fact a bug.
|
||||||
|
Invalid issues are very distracting and slow the pace at which NetBox is
|
||||||
|
developed.
|
||||||
|
|
||||||
* When submitting an issue, please be as descriptive as possible. Be sure to include:
|
* When submitting an issue, please be as descriptive as possible. Be
|
||||||
|
sure to include:
|
||||||
|
|
||||||
* The environment in which NetBox is running
|
* The 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
|
||||||
* Any error messages returned
|
applicable)
|
||||||
|
* Any error messages generated
|
||||||
* Screenshots (if applicable)
|
* 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
|
||||||
take some time for someone to address your issue.
|
much work is required to resolve them. It may take some time for someone
|
||||||
|
to address your issue.
|
||||||
|
|
||||||
## Feature Requests
|
## Feature Requests
|
||||||
|
|
||||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
|
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
|
||||||
requesting is already listed. (Be sure to search closed issues as well, since some feature requests are rejected.) If
|
requesting is already listed. (Be sure to search closed issues as well,
|
||||||
the feature you'd like to see has already been requested, click "add a reaction" in the top right corner of the issue
|
since some feature requests are rejected.) If the feature you'd like to
|
||||||
and add a thumbs up. This ensures that the issue has a better chance of making it onto the roadmap. Also feel free
|
see has already been requested, click "add a reaction" in the top right
|
||||||
to add a comment with any additional justification for the feature. (However, note that comments with no substance
|
corner of the issue and add a thumbs up (+1). This ensures that the
|
||||||
other than a "+1" will be deleted as spam. Please use GitHub's reactions feature to indicate your support.)
|
issue has a better chance of making it onto the roadmap. Also feel free
|
||||||
|
to add a comment with any additional justification for the feature.
|
||||||
|
(However, note that comments with no substance other than a "+1" will be
|
||||||
|
deleted. Please use GitHub's reactions feature to indicate your
|
||||||
|
support.)
|
||||||
|
|
||||||
* While suggestions for new features are 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
|
||||||
feature creep. For example, the following features would be firmly out of scope for NetBox:
|
limit the scope of NetBox's feature set to avoid feature creep. For
|
||||||
|
example, the following features would be firmly out of scope for NetBox:
|
||||||
|
|
||||||
* Ticket management
|
* Ticket management
|
||||||
* Network state monitoring
|
* Network state monitoring
|
||||||
* Acting as a DNS server
|
* Acting as a DNS server
|
||||||
* Acting as an authentication server
|
* Acting as an authentication server
|
||||||
|
|
||||||
* Before filing a new feature request, propose it on IRC or Reddit first. Feedback you receive there will help validate
|
* Before filing a new feature request, consider raising your idea on the
|
||||||
and shape the proposed feature before filing a formal issue.
|
mailing list first. Feedback you receive there will help validate and
|
||||||
|
shape the proposed feature before filing a formal issue.
|
||||||
|
|
||||||
* Good feature requests are very narrowly defined. Be sure to enumerate specific functionality and data schema. The more
|
* Good feature requests are very narrowly defined. Be sure to enumerate
|
||||||
effort you put into writing a feature request, the better its chances are of being implemented. Overly broad feature
|
specific functionality and data schema. The more effort you put into
|
||||||
requests will be closed.
|
writing a feature request, the better its chance is of being
|
||||||
|
implemented. Overly broad feature requests will be closed.
|
||||||
|
|
||||||
* When submitting a feature request on GitHub, be sure to include the following:
|
* When submitting a feature request on GitHub, be sure to include the
|
||||||
|
following:
|
||||||
|
|
||||||
* A detailed description of the proposed functionality
|
* A detailed description of the proposed functionality
|
||||||
* A use case for the feature; who would use it and what value it would add to NetBox
|
* A use case for the feature; who would use it and what value it
|
||||||
* A rough description of any changes necessary to the database schema
|
would add to NetBox
|
||||||
* Any third-party libraries or other resources which would be involved
|
* A rough description of changes necessary to the database schema
|
||||||
|
(if applicable)
|
||||||
|
* Any third-party libraries or other resources which would be
|
||||||
|
involved
|
||||||
|
|
||||||
## Submitting Pull Requests
|
## Submitting Pull Requests
|
||||||
|
|
||||||
* Be sure to open an issue before starting work on a pull request, and discuss your idea with the NetBox maintainers
|
* Be sure to open an issue before starting work on a pull request, and
|
||||||
before beginning work. This will help prevent wasting time on something that might we might not be able to implement.
|
discuss your idea with the NetBox maintainers before beginning work.
|
||||||
When suggesting a new feature, also make sure it won't conflict with any work that's already in progress.
|
This will help prevent wasting time on something that might we might not
|
||||||
|
be able to implement. When suggesting a new feature, also make sure it
|
||||||
|
won't conflict with any work that's already in progress.
|
||||||
|
|
||||||
* When submitting a pull request, please be sure to work off of the `develop` branch, rather than `master`. In NetBox,
|
* When submitting a pull request, please be sure to work off of the
|
||||||
the `develop` branch is used for ongoing development, while `master` is used for tagging new stable releases.
|
`develop` branch, rather than `master`. In NetBox, the `develop` branch
|
||||||
|
is used for ongoing development, while `master` is used for tagging new
|
||||||
|
stable releases.
|
||||||
|
|
||||||
* All code submissions should meet the following criteria (CI will enforce these checks):
|
* All code submissions should meet the following criteria (CI will
|
||||||
|
enforce these checks):
|
||||||
|
|
||||||
* Python syntax is valid
|
* Python syntax is valid
|
||||||
* All tests pass when run with `./manage.py test netbox/`
|
* All tests pass when run with `./manage.py test`
|
||||||
* PEP 8 compliance is enforced, with the exception that lines may be greater than 80 characters in length
|
* PEP 8 compliance is enforced, with the exception that lines may be
|
||||||
|
greater than 80 characters in length
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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/).
|
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
|
||||||
|
|
||||||
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss), or join us on IRC in **#netbox** on **irc.freenode.net**!
|
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss), or join us on IRC in **#netbox** on **irc.freenode.net**!
|
||||||
|
|
||||||
@@ -25,6 +25,9 @@ Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https
|
|||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
Please see [the documentation](http://netbox.readthedocs.io/en/latest/) for instructions on installing NetBox.
|
Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.
|
||||||
|
|
||||||
To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.
|
## Alternative Installations
|
||||||
|
|
||||||
|
* [Docker container](http://netbox.readthedocs.io/en/stable/installation/docker/)
|
||||||
|
* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
|
||||||
|
|||||||
@@ -2,31 +2,32 @@ The circuits component of NetBox deals with the management of long-haul Internet
|
|||||||
|
|
||||||
# Providers
|
# Providers
|
||||||
|
|
||||||
A provider is any entity which provides some form of connectivity. This obviously includes carriers which offer Internet and private transit service. However, it might also include Internet exchange (IX) points and even organizations with whom you peer directly.
|
A provider is any entity which provides some form of connectivity. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly.
|
||||||
|
|
||||||
Each provider may be assigned an autonomous system number (ASN) for reference. Each provider can also be assigned account and contact information, as well as miscellaneous comments.
|
Each provider may be assigned an autonomous system number (ASN), an account number, and contact information.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Circuits
|
# Circuits
|
||||||
|
|
||||||
A circuit represents a single physical data link connecting two endpoints. Each circuit belongs to a provider and must be assigned circuit ID which is unique to that provider. Each circuit must also be assigned to a site, and may optionally be connected to a specific interface on a specific device within that site.
|
A circuit represents a single physical data link connecting two endpoints. Each circuit belongs to a provider and must be assigned a circuit ID which is unique to that provider.
|
||||||
|
|
||||||
NetBox also tracks miscellaneous circuit attributes (most of which are optional), including:
|
|
||||||
|
|
||||||
* Date of installation
|
|
||||||
* Port speed
|
|
||||||
* Commit rate
|
|
||||||
* Cross-connect ID
|
|
||||||
* Patch panel information
|
|
||||||
|
|
||||||
### Circuit Types
|
### Circuit Types
|
||||||
|
|
||||||
Circuits can be classified by type. For example:
|
Circuits are classified by type. For example, you might define circuit types for:
|
||||||
|
|
||||||
* Internet transit
|
* Internet transit
|
||||||
* Out-of-band connectivity
|
* Out-of-band connectivity
|
||||||
* Peering
|
* Peering
|
||||||
* Private backhaul
|
* Private backhaul
|
||||||
|
|
||||||
Each circuit must be assigned exactly one circuit type.
|
Circuit types are fully customizable.
|
||||||
|
|
||||||
|
### Circuit Terminations
|
||||||
|
|
||||||
|
A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
|
||||||
|
|
||||||
|
Each circuit termination is tied to a site, and optionally to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit.
|
||||||
|
|||||||
@@ -2,61 +2,72 @@ Data center infrastructure management (DCIM) entails all physical assets: sites,
|
|||||||
|
|
||||||
# Sites
|
# Sites
|
||||||
|
|
||||||
How you define sites will depend on the nature of your organization, but typically a site will equate a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities.
|
How you choose to use sites will depend on the nature of your organization, but typically a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities.
|
||||||
|
|
||||||
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment.
|
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment, and an Autonomous System (AS) number.
|
||||||
|
|
||||||
|
### Regions
|
||||||
|
|
||||||
|
Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Racks
|
# Racks
|
||||||
|
|
||||||
Within each site exist one or more racks. Each rack within NetBox represents a physical two- or four-post equipment rack in which equipment is mounted. Rack height is measured in *rack units* (U); most racks are between 42U and 48U, but NetBox allows you to define racks of any height. Each rack has two faces (front and rear) on which devices can be mounted.
|
The rack model represents a physical two- or four-post equipment rack in which equipment is mounted. Each rack is assigned to a site. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U, but NetBox allows you to define racks of arbitrary height. Each rack has two faces (front and rear) on which devices can be mounted.
|
||||||
|
|
||||||
Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, M204.313) whereas internally you refer to is simply as "R113." The facility ID can alternatively be used to store a rack's serial number.
|
Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." The facility ID can alternatively be used to store a rack's serial number.
|
||||||
|
|
||||||
The available rack types include 2- and 4-post frames, 4-post cabinet, and wall-mounted frame and cabinet. Rail-to-rail width may be 19 or 23 inches.
|
The available rack types include 2- and 4-post frames, 4-post cabinet, and wall-mounted frame and cabinet. Rail-to-rail width may be 19 or 23 inches.
|
||||||
|
|
||||||
### Rack Groups
|
### Rack Groups
|
||||||
|
|
||||||
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site is a campus, each group might be a building. If each site is a building, each rack group might be a floor or room.
|
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
|
||||||
|
|
||||||
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not supported.
|
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not supported.
|
||||||
|
|
||||||
### Rack Roles
|
### Rack Roles
|
||||||
|
|
||||||
Each rak can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices.
|
Each rack can optionally be assigned a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable.
|
||||||
|
|
||||||
|
### Rack Space Reservations
|
||||||
|
|
||||||
|
Users can reserve units within a rack for future use. Multiple non-contiguous rack units can be associated with a single reservation (but reservations cannot span multiple racks).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Device Types
|
# Device Types
|
||||||
|
|
||||||
A device type represents a particular manufacturer and model of equipment. Device types describe the physical attributes of a device (rack height and depth), its class (e.g. console server, PDU, etc.), and its individual components (console, power, and data).
|
A device type represents a particular hardware model that exists in the real world. Device types describe the physical attributes of a device (rack height and depth), its class (e.g. console server, PDU, etc.), and its individual components (console, power, and data).
|
||||||
|
|
||||||
|
Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type.
|
||||||
|
|
||||||
### Manufacturers
|
### Manufacturers
|
||||||
|
|
||||||
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. Manufacturers are used to group different models of device.
|
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. The model number of a device type must be unique to its manufacturer.
|
||||||
|
|
||||||
### Component Templates
|
### Component Templates
|
||||||
|
|
||||||
Each device type is assigned a number of component templates which describe the console, power, and data ports a device has. These are:
|
Each device type is assigned a number of component templates which define the physical interfaces a device has. These are:
|
||||||
|
|
||||||
* Console port templates
|
* Console ports
|
||||||
* Console server port templates
|
* Console server ports
|
||||||
* Power port templates
|
* Power ports
|
||||||
* Power outlet templates
|
* Power outlets
|
||||||
* Interface templates
|
* Interfaces
|
||||||
* Device bay templates
|
* Device bays
|
||||||
|
|
||||||
Whenever a new device is created, it is automatically assigned console, power, and interface components per the templates assigned to its device type. For example, suppose your network employs Juniper EX4300-48T switches. You would create a device type with a model name "EX4300-48T" and assign it to the manufacturer "Juniper." You might then also create the following templates for it:
|
Whenever a new device is created, it is automatically assigned components per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates:
|
||||||
|
|
||||||
* One template for a console port ("Console")
|
* One template for a console port ("Console")
|
||||||
* Two templates for power ports ("PSU0" and "PSU1")
|
* Two templates for power ports ("PSU0" and "PSU1")
|
||||||
* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47")
|
* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47")
|
||||||
* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3")
|
* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3")
|
||||||
|
|
||||||
Once you've done this, every new device that you create as an instance of this type will automatically be assigned each of the components listed above.
|
Once component templates have been created, every new device that you create as an instance of this type will automatically be assigned each of the components listed above.
|
||||||
|
|
||||||
Note that assignment of components from templates occurs only at the time of device creation: If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components of existing devices individually.
|
!!! note
|
||||||
|
Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components of existing devices individually.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,19 +75,19 @@ Note that assignment of components from templates occurs only at the time of dev
|
|||||||
|
|
||||||
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
|
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
|
||||||
|
|
||||||
When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 shows as being mounted in U8.
|
When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 shows as being mounted in U8. This logic applies to racks with both ascending and descending unit numbering.
|
||||||
|
|
||||||
A device is said to be "full depth" if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede air flow.
|
A device is said to be "full depth" if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede air flow.
|
||||||
|
|
||||||
### Roles
|
### Roles
|
||||||
|
|
||||||
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, device can only belong to one device role.
|
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, a device can belong to only one role.
|
||||||
|
|
||||||
### Platforms
|
### Platforms
|
||||||
|
|
||||||
A device's platform is used to denote the type of software running on it. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
A device's platform is used to denote the type of software running on it. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
||||||
|
|
||||||
The assignment of platforms to devices is an entirely optional feature, and may be disregarded if not desired.
|
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
||||||
|
|
||||||
### Modules
|
### Modules
|
||||||
|
|
||||||
@@ -93,10 +104,11 @@ There are six types of device components which comprise all of the interconnecti
|
|||||||
* Interfaces
|
* Interfaces
|
||||||
* Device bays
|
* Device bays
|
||||||
|
|
||||||
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.)
|
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.) Each type of connection can be classified as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed.
|
||||||
|
|
||||||
Each type of connection can be classified as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed. In addition to a connecting peer, interfaces are also assigned a form factor and may be designated as management-only (for out-of-band management). Interfaces may also be assigned a short description.
|
Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned. Each interface can also be designated as management-only (for out-of-band management) and assigned a short description.
|
||||||
|
|
||||||
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
|
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
|
||||||
|
|
||||||
Note that child devices differ from modules in that they are still treated as independent devices, with their own console/power/data components, modules, and IP addresses. Modules, on the other hand, are parts within a device, such as a hard disk or power supply.
|
!!! note
|
||||||
|
Child devices differ from modules in that they are still treated as independent devices, with their own console/power/data components, modules, and IP addresses. Modules, on the other hand, are parts within a device, such as a hard disk or power supply, which do not provide their own management plane.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ This section entails features of NetBox which are not crucial to its primary fun
|
|||||||
|
|
||||||
# Custom Fields
|
# Custom Fields
|
||||||
|
|
||||||
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address` and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
||||||
|
|
||||||
However, some users might want to associate with objects attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number pointing to the support ticket that was opened to have it installed. This is certainly a legitimate use for NetBox, but it's perhaps not a common enough need to warrant expanding the internal data schema. Instead, you can create a custom field to hold this data.
|
However, some users might want to associate with objects attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number pointing to the support ticket that was opened to have it installed. This is certainly a legitimate use for NetBox, but it's perhaps not a common enough need to warrant expanding the internal data schema. Instead, you can create a custom field to hold this data.
|
||||||
|
|
||||||
@@ -33,7 +33,15 @@ NetBox allows users to define custom templates that can be used when exporting o
|
|||||||
|
|
||||||
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list.
|
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list.
|
||||||
|
|
||||||
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable. Typically, you'll want to iterate through this list using a for loop.
|
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
{% for rack in queryset %}
|
||||||
|
Rack: {{ rack.name }}
|
||||||
|
Site: {{ rack.site.name }}
|
||||||
|
Height: {{ rack.u_height }}U
|
||||||
|
{% endfor %}
|
||||||
|
```
|
||||||
|
|
||||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||||
|
|
||||||
@@ -44,10 +52,10 @@ A MIME type and file extension can optionally be defined for each export templat
|
|||||||
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
|
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
|
||||||
|
|
||||||
```
|
```
|
||||||
{% for d in queryset %}{% if d.status and d.primary_ip %}define host{
|
{% for device in queryset %}{% if device.status and device.primary_ip %}define host{
|
||||||
use generic-switch
|
use generic-switch
|
||||||
host_name {{ d.name }}
|
host_name {{ device.name }}
|
||||||
address {{ d.primary_ip.address.ip }}
|
address {{ device.primary_ip.address.ip }}
|
||||||
}
|
}
|
||||||
{% endif %}{% endfor %}
|
{% endif %}{% endfor %}
|
||||||
```
|
```
|
||||||
@@ -74,9 +82,9 @@ define host{
|
|||||||
|
|
||||||
# Graphs
|
# Graphs
|
||||||
|
|
||||||
NetBox does not generate graphs itself. This feature allows you to embed contextual graphs from an external resources inside certain NetBox views. Each embedded graph must be defined with the following parameters:
|
NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters:
|
||||||
|
|
||||||
* **Type:** Interface, provider, or site. This determines where the graph will be displayed.
|
* **Type:** Site, provider, or interface. This determines in which view the graph will be displayed.
|
||||||
* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
|
* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
|
||||||
* **Name:** The title to display above the graph.
|
* **Name:** The title to display above the graph.
|
||||||
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
||||||
@@ -86,7 +94,7 @@ NetBox does not generate graphs itself. This feature allows you to embed context
|
|||||||
|
|
||||||
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
||||||
|
|
||||||
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend connectivity).
|
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure).
|
||||||
|
|
||||||
To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
|
To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain
|
|||||||
|
|
||||||
Each VRF is assigned a name and a unique route distinguisher (RD). VRFs are an optional feature of NetBox: Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
|
Each VRF is assigned a name and a unique route distinguisher (RD). VRFs are an optional feature of NetBox: Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
By default, NetBox allows for overlapping IP space both in the global table and within each VRF. Unique space enforcement can be toggled per-VRF as well as in the global table using the `ENFORCE_GLOBAL_UNIQUE` configuration setting.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Aggregates
|
# Aggregates
|
||||||
|
|
||||||
IPv4 address space is organized as a hierarchy, with more-specific (smaller) prefix arranged as child nodes under less-specific (larger) prefixes. For example:
|
IP address space is organized as a hierarchy, with more-specific (smaller) prefixes arranged as child nodes under less-specific (larger) prefixes. For example:
|
||||||
|
|
||||||
* 10.0.0.0/8
|
* 10.0.0.0/8
|
||||||
* 10.1.0.0/16
|
* 10.1.0.0/16
|
||||||
@@ -18,23 +21,23 @@ IPv4 address space is organized as a hierarchy, with more-specific (smaller) pre
|
|||||||
|
|
||||||
The root of the IPv4 hierarchy is 0.0.0.0/0, which encompasses all possible IPv4 addresses (and similarly, ::/0 for IPv6). However, even the largest organizations use only a small fraction of the global address space. Therefore, it makes sense to track in NetBox only the address space which is of interest to your organization.
|
The root of the IPv4 hierarchy is 0.0.0.0/0, which encompasses all possible IPv4 addresses (and similarly, ::/0 for IPv6). However, even the largest organizations use only a small fraction of the global address space. Therefore, it makes sense to track in NetBox only the address space which is of interest to your organization.
|
||||||
|
|
||||||
Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the RFC 1918 private IPv4 space. So, you might define three aggregates for this space:
|
Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the private IPv4 space set aside in RFC 1918. So, you might define three aggregates for this space:
|
||||||
|
|
||||||
* 10.0.0.0/8
|
* 10.0.0.0/8
|
||||||
* 172.16.0.0/12
|
* 172.16.0.0/12
|
||||||
* 192.168.0.0/16
|
* 192.168.0.0/16
|
||||||
|
|
||||||
Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space.
|
Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space. (Most organizations will not have a need to track IPv6 link local space.)
|
||||||
|
|
||||||
Any prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation.
|
Prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation. Total utilization for each aggregate is displayed in the aggregates list.
|
||||||
|
|
||||||
Aggregates cannot overlap with one another; they can only exist in parallel. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix.
|
Aggregates cannot overlap with one another; they can only exist in parallel. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix and automatically grouped under 10.0.0.0/8.
|
||||||
|
|
||||||
### RIRs
|
### RIRs
|
||||||
|
|
||||||
Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space.
|
Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space.
|
||||||
|
|
||||||
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own).
|
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). Each RIR can be annotated as representing only private space.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -44,7 +47,7 @@ A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 19
|
|||||||
|
|
||||||
Each prefix may be assigned to one VRF; prefixes not assigned to a VRF are assigned to the "global" table. Prefixes are also organized under their respective aggregates, irrespective of VRF assignment.
|
Each prefix may be assigned to one VRF; prefixes not assigned to a VRF are assigned to the "global" table. Prefixes are also organized under their respective aggregates, irrespective of VRF assignment.
|
||||||
|
|
||||||
A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. This can be helpful is replicating real-world IP assignments. Each prefix may also be assigned a short description.
|
A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. Each prefix may also be assigned a short description.
|
||||||
|
|
||||||
### Statuses
|
### Statuses
|
||||||
|
|
||||||
@@ -52,7 +55,7 @@ Each prefix is assigned an operational status. This is one of the following:
|
|||||||
|
|
||||||
* Container - A summary of child prefixes
|
* Container - A summary of child prefixes
|
||||||
* Active - Provisioned and in use
|
* Active - Provisioned and in use
|
||||||
* Reserved - Earmarked for future use
|
* Reserved - Designated for future use
|
||||||
* Deprecated - No longer in use
|
* Deprecated - No longer in use
|
||||||
|
|
||||||
### Roles
|
### Roles
|
||||||
@@ -65,30 +68,32 @@ Whereas a status describes a prefix's operational state, a role describes its fu
|
|||||||
* Lab
|
* Lab
|
||||||
* Out-of-band
|
* Out-of-band
|
||||||
|
|
||||||
Role assignment is optional and you are free to create as many as you'd like.
|
Role assignment is optional and roles are fully customizable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# IP Addresses
|
# IP Addresses
|
||||||
|
|
||||||
An IP address comprises a single address (either IPv4 or IPv6) and its mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
|
An IP address comprises a single address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
|
||||||
|
|
||||||
Like prefixes, an IP address can optionally be assigned to a VRF (or it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs. Each IP address can also be assigned a short description.
|
Like prefixes, an IP address can optionally be assigned to a VRF (or it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs. Each IP address can also be assigned a short description.
|
||||||
|
|
||||||
Each IP address can optionally be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address.
|
An IP address can be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address (for both IPv4 and IPv6).
|
||||||
|
|
||||||
One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily is denoting the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not currently supported.
|
One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily to denote the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not supported.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# VLANs
|
# VLANs
|
||||||
|
|
||||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094). Note that while it is good practice, neither VLAN names nor IDs must be unique within a site. This is to accommodate the fact that many real-world network use less-than-optimal VLAN allocations and may have overlapping VLAN ID assignments in practice.
|
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site and/or VLAN group. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role, and may include a short description.
|
||||||
|
|
||||||
Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
|
### VLAN Groups
|
||||||
|
|
||||||
|
VLAN groups can be employed for administrative organization within NetBox. Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs, including within a site.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
|
|
||||||
A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, SSH (TCP/22). A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any IP address.)
|
A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ Roles are also used to control access to secrets. Each role is assigned an arbit
|
|||||||
|
|
||||||
Each user within NetBox can associate his or her account with an RSA public key. If activated by an administrator, this user key will contain a unique, encrypted copy of the AES master key needed to retrieve secret data.
|
Each user within NetBox can associate his or her account with an RSA public key. If activated by an administrator, this user key will contain a unique, encrypted copy of the AES master key needed to retrieve secret data.
|
||||||
|
|
||||||
User keys may be created by users individually, however they are of no use until they have been activated by a user who already has access to retrieve secret data.
|
User keys may be created by users individually, however they are of no use until they have been activated by a user who already possesses an active user key.
|
||||||
|
|
||||||
## Creating the First User Key
|
## Creating the First User Key
|
||||||
|
|
||||||
When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the super user) must create a user key. This can be done by navigating to Profile > User Key.
|
When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the superuser) must create a user key. This can be done by navigating to Profile > User Key.
|
||||||
|
|
||||||
To create a user key, you can either generate a new RSA key pair, or upload the public key belonging to a pair you already have. If generating a new key pair, **you must save the private key** locally before saving your new user key. Once your user key has been created, its public key will be displayed under your profile.
|
To create a user key, you can either generate a new RSA key pair, or upload the public key belonging to a pair you already have. If generating a new key pair, **you must save the private key** locally before saving your new user key. Once your user key has been created, its public key will be displayed under your profile.
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.
|
NetBox supports the assignment of resources to tenant organizations. Typically, these are used to represent individual customers of or internal departments within the organization using NetBox.
|
||||||
|
|
||||||
# Tenants
|
# 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.
|
A tenant represents a discrete organization. The following objects can be assigned to tenants:
|
||||||
|
|
||||||
The following objects can be assigned to tenants:
|
|
||||||
|
|
||||||
* Sites
|
* Sites
|
||||||
* Racks
|
* Racks
|
||||||
|
|||||||
@@ -2,12 +2,29 @@
|
|||||||
|
|
||||||
**Debian/Ubuntu**
|
**Debian/Ubuntu**
|
||||||
|
|
||||||
|
Python 3:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 2:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
|
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**CentOS/RHEL**
|
**CentOS/RHEL**
|
||||||
|
|
||||||
|
Python 3:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# yum install -y epel-release
|
||||||
|
# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 2:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# yum install -y epel-release
|
# yum install -y epel-release
|
||||||
# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
|
# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ If you followed the original installation guide to set up gunicorn, be sure to c
|
|||||||
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
|
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Copy the LDAP configuration if using LDAP:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py
|
||||||
|
```
|
||||||
|
|
||||||
## Option B: Clone the Git Repository (latest master release)
|
## Option B: Clone the Git Repository (latest master release)
|
||||||
|
|
||||||
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'circuits.apps.CircuitsConfig'
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'comments', 'terminations', 'custom_fields']
|
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||||
|
'terminations', 'custom_fields']
|
||||||
|
|
||||||
|
|
||||||
class CircuitNestedSerializer(CircuitSerializer):
|
class CircuitNestedSerializer(CircuitSerializer):
|
||||||
|
|||||||
9
netbox/circuits/apps.py
Normal file
9
netbox/circuits/apps.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitsConfig(AppConfig):
|
||||||
|
name = "circuits"
|
||||||
|
verbose_name = "Circuits"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import circuits.signals
|
||||||
@@ -11,8 +11,8 @@ from .models import Provider, Circuit, CircuitType
|
|||||||
|
|
||||||
|
|
||||||
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
@@ -31,7 +31,9 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
model = Provider
|
model = Provider
|
||||||
fields = ['name', 'account', 'asn']
|
fields = ['name', 'account', 'asn']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(account__icontains=value) |
|
Q(account__icontains=value) |
|
||||||
@@ -40,8 +42,8 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
@@ -93,10 +95,13 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['install_date']
|
fields = ['install_date']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(cid__icontains=value) |
|
Q(cid__icontains=value) |
|
||||||
Q(terminations__xconnect_id__icontains=value) |
|
Q(terminations__xconnect_id__icontains=value) |
|
||||||
Q(terminations__pp_info__icontains=value) |
|
Q(terminations__pp_info__icontains=value) |
|
||||||
|
Q(description__icontains=value) |
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
)
|
).distinct()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
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, VIRTUAL_IFACE_TYPES
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
@@ -62,7 +62,9 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
|
|
||||||
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Provider
|
model = Provider
|
||||||
|
q = forms.CharField(required=False, label='Search')
|
||||||
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
|
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
|
||||||
|
asn = forms.IntegerField(required=False, label='ASN')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -86,7 +88,7 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'comments']
|
fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'cid': "Unique circuit ID",
|
'cid': "Unique circuit ID",
|
||||||
'install_date': "Format: YYYY-MM-DD",
|
'install_date': "Format: YYYY-MM-DD",
|
||||||
@@ -104,7 +106,7 @@ class CircuitFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate']
|
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
|
||||||
|
|
||||||
|
|
||||||
class CircuitImportForm(BootstrapMixin, BulkImportForm):
|
class CircuitImportForm(BootstrapMixin, BulkImportForm):
|
||||||
@@ -117,22 +119,33 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
||||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||||
|
description = forms.CharField(max_length=100, required=False)
|
||||||
comments = CommentField(widget=SmallTextarea)
|
comments = CommentField(widget=SmallTextarea)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = ['tenant', 'commit_rate', 'comments']
|
nullable_fields = ['tenant', 'commit_rate', 'description', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
q = forms.CharField(required=False, label='Search')
|
||||||
to_field_name='slug')
|
type = FilterChoiceField(
|
||||||
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||||
to_field_name='slug')
|
to_field_name='slug'
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
|
)
|
||||||
null_option=(0, 'None'))
|
provider = FilterChoiceField(
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||||
to_field_name='slug')
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
site = FilterChoiceField(
|
||||||
|
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -140,19 +153,49 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
site = forms.ModelChoiceField(
|
||||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
|
queryset=Site.objects.all(),
|
||||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
|
widget=forms.Select(
|
||||||
attrs={'filter-for': 'device'}))
|
attrs={'filter-for': 'rack'}
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
)
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
)
|
||||||
display_field='display_name', attrs={'filter-for': 'interface'}))
|
rack = forms.ModelChoiceField(
|
||||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
queryset=Rack.objects.all(),
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
required=False,
|
||||||
|
label='Rack',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
|
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Device',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||||
|
display_field='display_name',
|
||||||
|
attrs={'filter-for': 'interface'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
livesearch = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Device',
|
||||||
|
widget=Livesearch(
|
||||||
|
query_key='q',
|
||||||
|
query_url='dcim-api:device_list',
|
||||||
|
field_to_update='device'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
interface = forms.ModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Interface',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
||||||
|
disabled_indicator='is_connected'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
|
|
||||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
|
||||||
disabled_indicator='is_connected'))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
@@ -194,14 +237,18 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
# Limit interface choices
|
# Limit interface choices
|
||||||
if self.is_bound and self.data.get('device'):
|
if self.is_bound and self.data.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.data['device'])\
|
interfaces = Interface.objects.filter(device=self.data['device']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||||
elif self.initial.get('device'):
|
elif self.initial.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
interfaces = Interface.objects.filter(device=self.initial['device']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||||
else:
|
else:
|
||||||
interfaces = []
|
interfaces = []
|
||||||
|
|||||||
20
netbox/circuits/migrations/0007_circuit_add_description.py
Normal file
20
netbox/circuits/migrations/0007_circuit_add_description.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-01-17 20:08
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0006_terminations'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
from extras.models import CustomFieldModel, CustomFieldValue
|
||||||
@@ -33,6 +34,7 @@ def humanize_speed(speed):
|
|||||||
return '{} Kbps'.format(speed)
|
return '{} Kbps'.format(speed)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Provider(CreatedUpdatedModel, CustomFieldModel):
|
class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||||
@@ -51,7 +53,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -67,6 +69,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class CircuitType(models.Model):
|
class CircuitType(models.Model):
|
||||||
"""
|
"""
|
||||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||||
@@ -78,13 +81,14 @@ class CircuitType(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
|
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||||
@@ -97,6 +101,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
|
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
|
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
|
||||||
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
|
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
|
||||||
|
description = models.CharField(max_length=100, blank=True)
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||||
|
|
||||||
@@ -104,7 +109,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
ordering = ['provider', 'cid']
|
ordering = ['provider', 'cid']
|
||||||
unique_together = ['provider', 'cid']
|
unique_together = ['provider', 'cid']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return u'{} {}'.format(self.provider, self.cid)
|
return u'{} {}'.format(self.provider, self.cid)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -118,6 +123,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
self.tenant.name if self.tenant else None,
|
self.tenant.name if self.tenant else None,
|
||||||
self.install_date.isoformat() if self.install_date else None,
|
self.install_date.isoformat() if self.install_date else None,
|
||||||
self.commit_rate,
|
self.commit_rate,
|
||||||
|
self.description,
|
||||||
])
|
])
|
||||||
|
|
||||||
def _get_termination(self, side):
|
def _get_termination(self, side):
|
||||||
@@ -139,6 +145,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
commit_rate_human.admin_order_field = 'commit_rate'
|
commit_rate_human.admin_order_field = 'commit_rate'
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class CircuitTermination(models.Model):
|
class CircuitTermination(models.Model):
|
||||||
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
|
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
|
||||||
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
|
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
|
||||||
@@ -154,12 +161,9 @@ class CircuitTermination(models.Model):
|
|||||||
ordering = ['circuit', 'term_side']
|
ordering = ['circuit', 'term_side']
|
||||||
unique_together = ['circuit', 'term_side']
|
unique_together = ['circuit', 'term_side']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||||
|
|
||||||
def get_parent_url(self):
|
|
||||||
return self.circuit.get_absolute_url()
|
|
||||||
|
|
||||||
def get_peer_termination(self):
|
def get_peer_termination(self):
|
||||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||||
try:
|
try:
|
||||||
|
|||||||
13
netbox/circuits/signals.py
Normal file
13
netbox/circuits/signals.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from django.db.models.signals import post_delete, post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .models import Circuit, CircuitTermination
|
||||||
|
|
||||||
|
|
||||||
|
@receiver((post_save, post_delete), sender=CircuitTermination)
|
||||||
|
def update_circuit(instance, **kwargs):
|
||||||
|
"""
|
||||||
|
When a CircuitTermination has been modified, update the last_updated time of its parent Circuit.
|
||||||
|
"""
|
||||||
|
Circuit.objects.filter(pk=instance.circuit_id).update(last_updated=timezone.now())
|
||||||
@@ -60,9 +60,8 @@ class CircuitTable(BaseTable):
|
|||||||
args=[Accessor('termination_a.site.slug')])
|
args=[Accessor('termination_a.site.slug')])
|
||||||
z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
|
z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
|
||||||
args=[Accessor('termination_z.site.slug')])
|
args=[Accessor('termination_z.site.slug')])
|
||||||
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
|
description = tables.Column(verbose_name='Description')
|
||||||
verbose_name='Commit Rate')
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'commit_rate')
|
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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.urlresolvers import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
@@ -24,14 +25,14 @@ class ProviderListView(ObjectListView):
|
|||||||
filter = filters.ProviderFilter
|
filter = filters.ProviderFilter
|
||||||
filter_form = forms.ProviderFilterForm
|
filter_form = forms.ProviderFilterForm
|
||||||
table = tables.ProviderTable
|
table = tables.ProviderTable
|
||||||
edit_permissions = ['circuits.change_provider', 'circuits.delete_provider']
|
|
||||||
template_name = 'circuits/provider_list.html'
|
template_name = 'circuits/provider_list.html'
|
||||||
|
|
||||||
|
|
||||||
def provider(request, slug):
|
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)
|
circuits = Circuit.objects.filter(provider=provider).select_related('type', 'tenant')\
|
||||||
|
.prefetch_related('terminations__site')
|
||||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||||
|
|
||||||
return render(request, 'circuits/provider.html', {
|
return render(request, 'circuits/provider.html', {
|
||||||
@@ -46,13 +47,13 @@ class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model = Provider
|
model = Provider
|
||||||
form_class = forms.ProviderForm
|
form_class = forms.ProviderForm
|
||||||
template_name = 'circuits/provider_edit.html'
|
template_name = 'circuits/provider_edit.html'
|
||||||
obj_list_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'circuits.delete_provider'
|
permission_required = 'circuits.delete_provider'
|
||||||
model = Provider
|
model = Provider
|
||||||
redirect_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -60,21 +61,23 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.ProviderImportForm
|
form = forms.ProviderImportForm
|
||||||
table = tables.ProviderTable
|
table = tables.ProviderTable
|
||||||
template_name = 'circuits/provider_import.html'
|
template_name = 'circuits/provider_import.html'
|
||||||
obj_list_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'circuits.change_provider'
|
permission_required = 'circuits.change_provider'
|
||||||
cls = Provider
|
cls = Provider
|
||||||
|
filter = filters.ProviderFilter
|
||||||
form = forms.ProviderBulkEditForm
|
form = forms.ProviderBulkEditForm
|
||||||
template_name = 'circuits/provider_bulk_edit.html'
|
template_name = 'circuits/provider_bulk_edit.html'
|
||||||
default_redirect_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_provider'
|
permission_required = 'circuits.delete_provider'
|
||||||
cls = Provider
|
cls = Provider
|
||||||
default_redirect_url = 'circuits:provider_list'
|
filter = filters.ProviderFilter
|
||||||
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -84,7 +87,6 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
class CircuitTypeListView(ObjectListView):
|
class CircuitTypeListView(ObjectListView):
|
||||||
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||||
table = tables.CircuitTypeTable
|
table = tables.CircuitTypeTable
|
||||||
edit_permissions = ['circuits.change_circuittype', 'circuits.delete_circuittype']
|
|
||||||
template_name = 'circuits/circuittype_list.html'
|
template_name = 'circuits/circuittype_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -92,14 +94,15 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'circuits.change_circuittype'
|
permission_required = 'circuits.change_circuittype'
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
form_class = forms.CircuitTypeForm
|
form_class = forms.CircuitTypeForm
|
||||||
obj_list_url = 'circuits:circuittype_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('circuits:circuittype_list')
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_circuittype'
|
permission_required = 'circuits.delete_circuittype'
|
||||||
cls = CircuitType
|
cls = CircuitType
|
||||||
default_redirect_url = 'circuits:circuittype_list'
|
default_return_url = 'circuits:circuittype_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -111,15 +114,22 @@ class CircuitListView(ObjectListView):
|
|||||||
filter = filters.CircuitFilter
|
filter = filters.CircuitFilter
|
||||||
filter_form = forms.CircuitFilterForm
|
filter_form = forms.CircuitFilterForm
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
edit_permissions = ['circuits.change_circuit', 'circuits.delete_circuit']
|
|
||||||
template_name = 'circuits/circuit_list.html'
|
template_name = 'circuits/circuit_list.html'
|
||||||
|
|
||||||
|
|
||||||
def circuit(request, pk):
|
def circuit(request, pk):
|
||||||
|
|
||||||
circuit = get_object_or_404(Circuit, pk=pk)
|
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
|
||||||
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
|
termination_a = CircuitTermination.objects.select_related(
|
||||||
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
|
'site__region', 'interface__device'
|
||||||
|
).filter(
|
||||||
|
circuit=circuit, term_side=TERM_SIDE_A
|
||||||
|
).first()
|
||||||
|
termination_z = CircuitTermination.objects.select_related(
|
||||||
|
'site__region', 'interface__device'
|
||||||
|
).filter(
|
||||||
|
circuit=circuit, term_side=TERM_SIDE_Z
|
||||||
|
).first()
|
||||||
|
|
||||||
return render(request, 'circuits/circuit.html', {
|
return render(request, 'circuits/circuit.html', {
|
||||||
'circuit': circuit,
|
'circuit': circuit,
|
||||||
@@ -134,13 +144,13 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
form_class = forms.CircuitForm
|
form_class = forms.CircuitForm
|
||||||
fields_initial = ['provider']
|
fields_initial = ['provider']
|
||||||
template_name = 'circuits/circuit_edit.html'
|
template_name = 'circuits/circuit_edit.html'
|
||||||
obj_list_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'circuits.delete_circuit'
|
permission_required = 'circuits.delete_circuit'
|
||||||
model = Circuit
|
model = Circuit
|
||||||
redirect_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -148,21 +158,23 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.CircuitImportForm
|
form = forms.CircuitImportForm
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
template_name = 'circuits/circuit_import.html'
|
template_name = 'circuits/circuit_import.html'
|
||||||
obj_list_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'circuits.change_circuit'
|
permission_required = 'circuits.change_circuit'
|
||||||
cls = Circuit
|
cls = Circuit
|
||||||
|
filter = filters.CircuitFilter
|
||||||
form = forms.CircuitBulkEditForm
|
form = forms.CircuitBulkEditForm
|
||||||
template_name = 'circuits/circuit_bulk_edit.html'
|
template_name = 'circuits/circuit_bulk_edit.html'
|
||||||
default_redirect_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_circuit'
|
permission_required = 'circuits.delete_circuit'
|
||||||
cls = Circuit
|
cls = Circuit
|
||||||
default_redirect_url = 'circuits:circuit_list'
|
filter = filters.CircuitFilter
|
||||||
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
@permission_required('circuits.change_circuittermination')
|
@permission_required('circuits.change_circuittermination')
|
||||||
@@ -206,7 +218,7 @@ def circuit_terminations_swap(request, pk):
|
|||||||
'form': form,
|
'form': form,
|
||||||
'panel_class': 'default',
|
'panel_class': 'default',
|
||||||
'button_class': 'primary',
|
'button_class': 'primary',
|
||||||
'cancel_url': circuit.get_absolute_url(),
|
'return_url': circuit.get_absolute_url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -221,12 +233,14 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
fields_initial = ['term_side']
|
fields_initial = ['term_side']
|
||||||
template_name = 'circuits/circuittermination_edit.html'
|
template_name = 'circuits/circuittermination_edit.html'
|
||||||
|
|
||||||
def alter_obj(self, obj, args, kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
if 'circuit' in kwargs:
|
if 'circuit' in url_kwargs:
|
||||||
circuit = get_object_or_404(Circuit, pk=kwargs['circuit'])
|
obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit'])
|
||||||
obj.circuit = circuit
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return obj.circuit.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'circuits.delete_circuittermination'
|
permission_required = 'circuits.delete_circuittermination'
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
|
from mptt.admin import MPTTModelAdmin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
|
||||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
|
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region,
|
||||||
|
Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Region)
|
||||||
|
class RegionAdmin(MPTTModelAdmin):
|
||||||
|
list_display = ['name', 'parent', 'slug']
|
||||||
|
prepopulated_fields = {
|
||||||
|
'slug': ['name'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Site)
|
@admin.register(Site)
|
||||||
class SiteAdmin(admin.ModelAdmin):
|
class SiteAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'slug', 'facility', 'asn']
|
list_display = ['name', 'slug', 'facility', 'asn']
|
||||||
@@ -37,6 +48,11 @@ class RackAdmin(admin.ModelAdmin):
|
|||||||
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
|
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(RackReservation)
|
||||||
|
class RackRackReservationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['rack', 'units', 'description', 'user', 'created']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -4,23 +4,42 @@ from ipam.models import IPAddress
|
|||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
|
||||||
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_FRONT,
|
||||||
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
RACK_FACE_REAR, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
||||||
)
|
)
|
||||||
from extras.api.serializers import CustomFieldSerializer
|
from extras.api.serializers import CustomFieldSerializer
|
||||||
from tenancy.api.serializers import TenantNestedSerializer
|
from tenancy.api.serializers import TenantNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
class RegionNestedSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Region
|
||||||
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
|
class RegionSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Region
|
||||||
|
fields = ['id', 'name', 'slug', 'parent']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||||
|
region = RegionNestedSerializer()
|
||||||
tenant = TenantNestedSerializer()
|
tenant = TenantNestedSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
fields = ['id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||||
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
|
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
|
||||||
'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
||||||
|
|
||||||
@@ -70,6 +89,12 @@ class RackRoleNestedSerializer(RackRoleSerializer):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class RackReservationNestedSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackReservation
|
||||||
|
fields = ['id', 'units', 'created', 'user', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RackSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
class RackSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||||
site = SiteNestedSerializer()
|
site = SiteNestedSerializer()
|
||||||
@@ -92,10 +117,11 @@ class RackNestedSerializer(RackSerializer):
|
|||||||
class RackDetailSerializer(RackSerializer):
|
class RackDetailSerializer(RackSerializer):
|
||||||
front_units = serializers.SerializerMethodField()
|
front_units = serializers.SerializerMethodField()
|
||||||
rear_units = serializers.SerializerMethodField()
|
rear_units = serializers.SerializerMethodField()
|
||||||
|
reservations = RackReservationNestedSerializer(many=True)
|
||||||
|
|
||||||
class Meta(RackSerializer.Meta):
|
class Meta(RackSerializer.Meta):
|
||||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
|
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
|
||||||
'u_height', 'desc_units', 'comments', 'custom_fields', 'front_units', 'rear_units']
|
'u_height', 'desc_units', 'reservations', 'comments', 'custom_fields', '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)
|
||||||
@@ -110,6 +136,18 @@ class RackDetailSerializer(RackSerializer):
|
|||||||
return units
|
return units
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationSerializer(serializers.ModelSerializer):
|
||||||
|
rack = RackNestedSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackReservation
|
||||||
|
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
@@ -134,11 +172,13 @@ class ManufacturerNestedSerializer(ManufacturerSerializer):
|
|||||||
class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||||
manufacturer = ManufacturerNestedSerializer()
|
manufacturer = ManufacturerNestedSerializer()
|
||||||
subdevice_role = serializers.SerializerMethodField()
|
subdevice_role = serializers.SerializerMethodField()
|
||||||
|
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields']
|
'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
|
||||||
|
'comments', 'custom_fields', 'instance_count']
|
||||||
|
|
||||||
def get_subdevice_role(self, obj):
|
def get_subdevice_role(self, obj):
|
||||||
return {
|
return {
|
||||||
@@ -198,9 +238,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer):
|
|||||||
|
|
||||||
class Meta(DeviceTypeSerializer.Meta):
|
class Meta(DeviceTypeSerializer.Meta):
|
||||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
|
||||||
'console_port_templates', 'cs_port_templates', 'power_port_templates', 'power_outlet_templates',
|
'comments', 'custom_fields', 'console_port_templates', 'cs_port_templates', 'power_port_templates',
|
||||||
'interface_templates']
|
'power_outlet_templates', 'interface_templates']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -254,6 +294,7 @@ class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
|||||||
device_role = DeviceRoleNestedSerializer()
|
device_role = DeviceRoleNestedSerializer()
|
||||||
tenant = TenantNestedSerializer()
|
tenant = TenantNestedSerializer()
|
||||||
platform = PlatformNestedSerializer()
|
platform = PlatformNestedSerializer()
|
||||||
|
site = SiteNestedSerializer()
|
||||||
rack = RackNestedSerializer()
|
rack = RackNestedSerializer()
|
||||||
primary_ip = DeviceIPAddressNestedSerializer()
|
primary_ip = DeviceIPAddressNestedSerializer()
|
||||||
primary_ip4 = DeviceIPAddressNestedSerializer()
|
primary_ip4 = DeviceIPAddressNestedSerializer()
|
||||||
@@ -262,9 +303,11 @@ class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
|
fields = [
|
||||||
'asset_tag', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||||
'primary_ip6', 'comments', 'custom_fields']
|
'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||||
|
'comments', 'custom_fields',
|
||||||
|
]
|
||||||
|
|
||||||
def get_parent_device(self, obj):
|
def get_parent_device(self, obj):
|
||||||
try:
|
try:
|
||||||
@@ -366,13 +409,24 @@ class PowerPortNestedSerializer(PowerPortSerializer):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceSerializer(serializers.ModelSerializer):
|
class LAGInterfaceNestedSerializer(serializers.ModelSerializer):
|
||||||
device = DeviceNestedSerializer()
|
|
||||||
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected']
|
fields = ['id', 'name', 'form_factor']
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceSerializer(serializers.ModelSerializer):
|
||||||
|
device = DeviceNestedSerializer()
|
||||||
|
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
||||||
|
lag = LAGInterfaceNestedSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Interface
|
||||||
|
fields = [
|
||||||
|
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class InterfaceNestedSerializer(InterfaceSerializer):
|
class InterfaceNestedSerializer(InterfaceSerializer):
|
||||||
@@ -386,8 +440,10 @@ class InterfaceDetailSerializer(InterfaceSerializer):
|
|||||||
connected_interface = InterfaceSerializer()
|
connected_interface = InterfaceSerializer()
|
||||||
|
|
||||||
class Meta(InterfaceSerializer.Meta):
|
class Meta(InterfaceSerializer.Meta):
|
||||||
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
fields = [
|
||||||
'connected_interface']
|
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
||||||
|
'connected_interface',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ from .views import *
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
# Regions
|
||||||
|
url(r'^regions/$', RegionListView.as_view(), name='region_list'),
|
||||||
|
url(r'^regions/(?P<pk>\d+)/$', RegionDetailView.as_view(), name='region_detail'),
|
||||||
|
|
||||||
# Sites
|
# Sites
|
||||||
url(r'^sites/$', SiteListView.as_view(), name='site_list'),
|
url(r'^sites/$', SiteListView.as_view(), name='site_list'),
|
||||||
url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),
|
url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),
|
||||||
@@ -27,6 +31,10 @@ urlpatterns = [
|
|||||||
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
|
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
|
||||||
url(r'^racks/(?P<pk>\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'),
|
url(r'^racks/(?P<pk>\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'),
|
||||||
|
|
||||||
|
# Rack reservations
|
||||||
|
url(r'^rack-reservations/$', RackReservationListView.as_view(), name='rackreservation_list'),
|
||||||
|
url(r'^rack-reservations/(?P<pk>\d+)/$', RackReservationDetailView.as_view(), name='rackreservation_detail'),
|
||||||
|
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
url(r'^manufacturers/$', ManufacturerListView.as_view(), name='manufacturer_list'),
|
url(r'^manufacturers/$', ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||||
url(r'^manufacturers/(?P<pk>\d+)/$', ManufacturerDetailView.as_view(), name='manufacturer_detail'),
|
url(r'^manufacturers/(?P<pk>\d+)/$', ManufacturerDetailView.as_view(), name='manufacturer_detail'),
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ from django.http import Http404
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
|
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection,
|
||||||
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||||
|
VIRTUAL_IFACE_TYPES,
|
||||||
)
|
)
|
||||||
from dcim import filters
|
from dcim import filters
|
||||||
from extras.api.views import CustomFieldModelAPIView
|
from extras.api.views import CustomFieldModelAPIView
|
||||||
@@ -21,6 +22,26 @@ from .exceptions import MissingFilterException
|
|||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
class RegionListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
List all regions
|
||||||
|
"""
|
||||||
|
queryset = Region.objects.all()
|
||||||
|
serializer_class = serializers.RegionSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class RegionDetailView(generics.RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
Retrieve a single region
|
||||||
|
"""
|
||||||
|
queryset = Region.objects.all()
|
||||||
|
serializer_class = serializers.RegionSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
@@ -134,6 +155,27 @@ class RackUnitListView(APIView):
|
|||||||
return Response(elevation)
|
return Response(elevation)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
List all rack reservation
|
||||||
|
"""
|
||||||
|
queryset = RackReservation.objects.all()
|
||||||
|
serializer_class = serializers.RackReservationSerializer
|
||||||
|
filter_class = filters.RackReservationFilter
|
||||||
|
|
||||||
|
|
||||||
|
class RackReservationDetailView(generics.RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
Retrieve a single rack reservation
|
||||||
|
"""
|
||||||
|
queryset = RackReservation.objects.all()
|
||||||
|
serializer_class = serializers.RackReservationSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
@@ -331,14 +373,15 @@ class InterfaceListView(generics.ListAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
||||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||||
queryset = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b')
|
queryset = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
|
||||||
|
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination')
|
||||||
|
|
||||||
# Filter by type (physical or virtual)
|
# Filter by type (physical or virtual)
|
||||||
iface_type = self.request.query_params.get('type')
|
iface_type = self.request.query_params.get('type')
|
||||||
if iface_type == 'physical':
|
if iface_type == 'physical':
|
||||||
queryset = queryset.exclude(form_factor=IFACE_FF_VIRTUAL)
|
queryset = queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||||
elif iface_type == 'virtual':
|
elif iface_type == 'virtual':
|
||||||
queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL)
|
queryset = queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||||
elif iface_type is not None:
|
elif iface_type is not None:
|
||||||
queryset = queryset.empty()
|
queryset = queryset.empty()
|
||||||
|
|
||||||
@@ -489,8 +532,8 @@ class RelatedConnectionsView(APIView):
|
|||||||
response['power-ports'].append(data)
|
response['power-ports'].append(data)
|
||||||
|
|
||||||
# Interface connections
|
# Interface connections
|
||||||
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
|
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
|
||||||
'circuit_termination')
|
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination')
|
||||||
for iface in interfaces:
|
for iface in interfaces:
|
||||||
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
||||||
del(data['device'])
|
del(data['device'])
|
||||||
|
|||||||
@@ -7,16 +7,28 @@ from extras.filters import CustomFieldFilterSet
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||||
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||||
|
VIRTUAL_IFACE_TYPES,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
region_id = NullableModelMultipleChoiceFilter(
|
||||||
|
name='region',
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
label='Region (ID)',
|
||||||
|
)
|
||||||
|
region = NullableModelMultipleChoiceFilter(
|
||||||
|
name='region',
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Region (slug)',
|
||||||
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
@@ -33,9 +45,16 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
model = Site
|
model = Site
|
||||||
fields = ['q', 'name', 'facility', 'asn']
|
fields = ['q', 'name', 'facility', 'asn']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
|
if not value.strip():
|
||||||
Q(shipping_address__icontains=value) | Q(comments__icontains=value)
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(facility__icontains=value) |
|
||||||
|
Q(physical_address__icontains=value) |
|
||||||
|
Q(shipping_address__icontains=value) |
|
||||||
|
Q(comments__icontains=value)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
qs_filter |= Q(asn=int(value.strip()))
|
qs_filter |= Q(asn=int(value.strip()))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -58,11 +77,12 @@ class RackGroupFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
@@ -114,7 +134,9 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
model = Rack
|
model = Rack
|
||||||
fields = ['u_height']
|
fields = ['u_height']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(facility_id__icontains=value) |
|
Q(facility_id__icontains=value) |
|
||||||
@@ -122,9 +144,21 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RackReservationFilter(django_filters.FilterSet):
|
||||||
|
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='rack',
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
label='Rack (ID)',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackReservation
|
||||||
|
fields = ['rack', 'user']
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
@@ -141,10 +175,13 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device',
|
fields = [
|
||||||
'subdevice_role']
|
'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
|
||||||
|
]
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(manufacturer__name__icontains=value) |
|
Q(manufacturer__name__icontains=value) |
|
||||||
Q(model__icontains=value) |
|
Q(model__icontains=value) |
|
||||||
@@ -154,21 +191,21 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
mac_address = django_filters.MethodFilter(
|
mac_address = django_filters.CharFilter(
|
||||||
action='_mac_address',
|
method='_mac_address',
|
||||||
label='MAC address',
|
label='MAC address',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack__site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack__site__slug',
|
name='site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site name (slug)',
|
label='Site name (slug)',
|
||||||
@@ -178,7 +215,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
label='Rack group (ID)',
|
label='Rack group (ID)',
|
||||||
)
|
)
|
||||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
rack_id = NullableModelMultipleChoiceFilter(
|
||||||
name='rack',
|
name='rack',
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
label='Rack (ID)',
|
label='Rack (ID)',
|
||||||
@@ -259,7 +296,9 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
model = Device
|
model = Device
|
||||||
fields = ['name', 'serial', 'asset_tag']
|
fields = ['name', 'serial', 'asset_tag']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(serial__icontains=value.strip()) |
|
Q(serial__icontains=value.strip()) |
|
||||||
@@ -268,7 +307,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
def _mac_address(self, queryset, value):
|
def _mac_address(self, queryset, name, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
if not value:
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
@@ -362,58 +401,72 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
)
|
)
|
||||||
|
type = django_filters.CharFilter(
|
||||||
|
method='filter_type',
|
||||||
|
label='Interface type',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['name']
|
fields = ['name']
|
||||||
|
|
||||||
|
def filter_type(self, queryset, name, value):
|
||||||
|
value = value.strip().lower()
|
||||||
|
if value == 'physical':
|
||||||
|
return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||||
|
elif value == 'virtual':
|
||||||
|
return queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||||
|
elif value == 'lag':
|
||||||
|
return queryset.filter(form_factor=IFACE_FF_LAG)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||||
site = django_filters.MethodFilter(
|
site = django_filters.CharFilter(
|
||||||
action='filter_site',
|
method='filter_site',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
|
fields = []
|
||||||
|
|
||||||
def filter_site(self, queryset, value):
|
def filter_site(self, queryset, name, value):
|
||||||
value = value.strip()
|
if not value.strip():
|
||||||
if not value:
|
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(cs_port__device__rack__site__slug=value)
|
return queryset.filter(cs_port__device__site__slug=value)
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionFilter(django_filters.FilterSet):
|
class PowerConnectionFilter(django_filters.FilterSet):
|
||||||
site = django_filters.MethodFilter(
|
site = django_filters.CharFilter(
|
||||||
action='filter_site',
|
method='filter_site',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
|
fields = []
|
||||||
|
|
||||||
def filter_site(self, queryset, value):
|
def filter_site(self, queryset, name, value):
|
||||||
value = value.strip()
|
if not value.strip():
|
||||||
if not value:
|
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(power_outlet__device__rack__site__slug=value)
|
return queryset.filter(power_outlet__device__site__slug=value)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionFilter(django_filters.FilterSet):
|
class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||||
site = django_filters.MethodFilter(
|
site = django_filters.CharFilter(
|
||||||
action='filter_site',
|
method='filter_site',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceConnection
|
model = InterfaceConnection
|
||||||
|
fields = []
|
||||||
|
|
||||||
def filter_site(self, queryset, value):
|
def filter_site(self, queryset, name, value):
|
||||||
value = value.strip()
|
if not value.strip():
|
||||||
if not value:
|
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(interface_a__device__rack__site__slug=value) |
|
Q(interface_a__device__site__slug=value) |
|
||||||
Q(interface_b__device__rack__site__slug=value)
|
Q(interface_b__device__site__slug=value)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1915,6 +1915,7 @@
|
|||||||
"platform": 1,
|
"platform": 1,
|
||||||
"name": "test1-edge1",
|
"name": "test1-edge1",
|
||||||
"serial": "5555555555",
|
"serial": "5555555555",
|
||||||
|
"site": 1,
|
||||||
"rack": 1,
|
"rack": 1,
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
@@ -1935,6 +1936,7 @@
|
|||||||
"platform": 1,
|
"platform": 1,
|
||||||
"name": "test1-core1",
|
"name": "test1-core1",
|
||||||
"serial": "",
|
"serial": "",
|
||||||
|
"site": 1,
|
||||||
"rack": 1,
|
"rack": 1,
|
||||||
"position": 17,
|
"position": 17,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
@@ -1955,6 +1957,7 @@
|
|||||||
"platform": 1,
|
"platform": 1,
|
||||||
"name": "test1-spine1",
|
"name": "test1-spine1",
|
||||||
"serial": "",
|
"serial": "",
|
||||||
|
"site": 1,
|
||||||
"rack": 1,
|
"rack": 1,
|
||||||
"position": 33,
|
"position": 33,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
@@ -1975,6 +1978,7 @@
|
|||||||
"platform": 1,
|
"platform": 1,
|
||||||
"name": "test1-leaf1",
|
"name": "test1-leaf1",
|
||||||
"serial": "",
|
"serial": "",
|
||||||
|
"site": 1,
|
||||||
"rack": 1,
|
"rack": 1,
|
||||||
"position": 34,
|
"position": 34,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
@@ -1995,6 +1999,7 @@
|
|||||||
"platform": 1,
|
"platform": 1,
|
||||||
"name": "test1-leaf2",
|
"name": "test1-leaf2",
|
||||||
"serial": "9823478293748",
|
"serial": "9823478293748",
|
||||||
|
"site": 1,
|
||||||
"rack": 2,
|
"rack": 2,
|
||||||
"position": 34,
|
"position": 34,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
@@ -2015,6 +2020,7 @@
|
|||||||
"platform": 1,
|
"platform": 1,
|
||||||
"name": "test1-spine2",
|
"name": "test1-spine2",
|
||||||
"serial": "45649818158",
|
"serial": "45649818158",
|
||||||
|
"site": 1,
|
||||||
"rack": 2,
|
"rack": 2,
|
||||||
"position": 33,
|
"position": 33,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
@@ -2035,6 +2041,7 @@
|
|||||||
"platform": 1,
|
"platform": 1,
|
||||||
"name": "test1-edge2",
|
"name": "test1-edge2",
|
||||||
"serial": "7567356345",
|
"serial": "7567356345",
|
||||||
|
"site": 1,
|
||||||
"rack": 2,
|
"rack": 2,
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
@@ -2055,6 +2062,7 @@
|
|||||||
"platform": 1,
|
"platform": 1,
|
||||||
"name": "test1-core2",
|
"name": "test1-core2",
|
||||||
"serial": "67856734534",
|
"serial": "67856734534",
|
||||||
|
"site": 1,
|
||||||
"rack": 2,
|
"rack": 2,
|
||||||
"position": 17,
|
"position": 17,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
@@ -2075,6 +2083,7 @@
|
|||||||
"platform": 2,
|
"platform": 2,
|
||||||
"name": "test1-oob1",
|
"name": "test1-oob1",
|
||||||
"serial": "98273942938",
|
"serial": "98273942938",
|
||||||
|
"site": 1,
|
||||||
"rack": 1,
|
"rack": 1,
|
||||||
"position": 42,
|
"position": 42,
|
||||||
"face": 0,
|
"face": 0,
|
||||||
@@ -2095,6 +2104,7 @@
|
|||||||
"platform": null,
|
"platform": null,
|
||||||
"name": "test1-pdu1",
|
"name": "test1-pdu1",
|
||||||
"serial": "",
|
"serial": "",
|
||||||
|
"site": 1,
|
||||||
"rack": 1,
|
"rack": 1,
|
||||||
"position": null,
|
"position": null,
|
||||||
"face": null,
|
"face": null,
|
||||||
@@ -2115,6 +2125,7 @@
|
|||||||
"platform": null,
|
"platform": null,
|
||||||
"name": "test1-pdu2",
|
"name": "test1-pdu2",
|
||||||
"serial": "",
|
"serial": "",
|
||||||
|
"site": 1,
|
||||||
"rack": 2,
|
"rack": 2,
|
||||||
"position": null,
|
"position": null,
|
||||||
"face": null,
|
"face": null,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
from mptt.forms import TreeNodeChoiceField
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
@@ -8,18 +11,19 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
|||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
|
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
|
||||||
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
|
||||||
SlugField,
|
SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from formfields import MACAddressFormField
|
from .formfields import MACAddressFormField
|
||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||||
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
||||||
Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
||||||
|
VIRTUAL_IFACE_TYPES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,18 +56,42 @@ def validate_connection_status(value):
|
|||||||
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceComponentForm(BootstrapMixin, forms.Form):
|
||||||
|
"""
|
||||||
|
Allow inclusion of the parent device as context for limiting field choices.
|
||||||
|
"""
|
||||||
|
def __init__(self, device, *args, **kwargs):
|
||||||
|
self.device = device
|
||||||
|
super(DeviceComponentForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
class RegionForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
slug = SlugField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Region
|
||||||
|
fields = ['parent', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteForm(BootstrapMixin, CustomFieldForm):
|
class SiteForm(BootstrapMixin, CustomFieldForm):
|
||||||
|
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name',
|
fields = [
|
||||||
'contact_phone', 'contact_email', 'comments']
|
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||||
|
'contact_name', 'contact_phone', 'contact_email', '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}),
|
||||||
@@ -78,12 +106,22 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
|
|||||||
|
|
||||||
|
|
||||||
class SiteFromCSVForm(forms.ModelForm):
|
class SiteFromCSVForm(forms.ModelForm):
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
region = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
Region.objects.all(), to_field_name='name', required=False, error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
|
fields = [
|
||||||
|
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class SiteImportForm(BootstrapMixin, BulkImportForm):
|
class SiteImportForm(BootstrapMixin, BulkImportForm):
|
||||||
@@ -92,17 +130,27 @@ class SiteImportForm(BootstrapMixin, BulkImportForm):
|
|||||||
|
|
||||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
|
||||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
|
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = ['tenant', 'asn']
|
nullable_fields = ['region', 'tenant', 'asn']
|
||||||
|
|
||||||
|
|
||||||
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Site
|
model = Site
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
|
q = forms.CharField(required=False, label='Search')
|
||||||
null_option=(0, 'None'))
|
region = FilterTreeNodeMultipleChoiceField(
|
||||||
|
queryset=Region.objects.annotate(filter_count=Count('sites')),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -232,13 +280,54 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
|
|
||||||
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Rack
|
model = Rack
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug')
|
q = forms.CharField(required=False, label='Search')
|
||||||
group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site')
|
site = FilterChoiceField(
|
||||||
.annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None'))
|
queryset=Site.objects.annotate(filter_count=Count('racks')),
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
|
to_field_name='slug'
|
||||||
null_option=(0, 'None'))
|
)
|
||||||
role = FilterChoiceField(queryset=RackRole.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
|
group_id = FilterChoiceField(
|
||||||
null_option=(0, 'None'))
|
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
|
||||||
|
label='Rack group',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.annotate(filter_count=Count('racks')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
role = FilterChoiceField(
|
||||||
|
queryset=RackRole.objects.annotate(filter_count=Count('racks')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackReservation
|
||||||
|
fields = ['units', 'description']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
super(RackReservationForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Populate rack unit choices
|
||||||
|
self.fields['units'].widget.choices = self._get_unit_choices()
|
||||||
|
|
||||||
|
def _get_unit_choices(self):
|
||||||
|
rack = self.instance.rack
|
||||||
|
reserved_units = []
|
||||||
|
for resv in rack.reservations.exclude(pk=self.instance.pk):
|
||||||
|
for u in resv.units:
|
||||||
|
reserved_units.append(u)
|
||||||
|
unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
|
||||||
|
return unit_choices
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -263,13 +352,17 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||||
'is_pdu', 'is_network_device', 'subdevice_role', 'comments']
|
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
|
||||||
|
labels = {
|
||||||
|
'interface_ordering': 'Order interfaces by',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||||
u_height = forms.IntegerField(min_value=1, required=False)
|
u_height = forms.IntegerField(min_value=1, required=False)
|
||||||
|
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = []
|
nullable_fields = []
|
||||||
@@ -277,8 +370,11 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
|
|
||||||
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
q = forms.CharField(required=False, label='Search')
|
||||||
to_field_name='slug')
|
manufacturer = FilterChoiceField(
|
||||||
|
queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -295,7 +391,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
|
class ConsolePortTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@@ -309,7 +405,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
class ConsoleServerPortTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@@ -323,7 +419,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
class PowerPortTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@@ -337,7 +433,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
|
class PowerOutletTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@@ -351,7 +447,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form):
|
class InterfaceTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||||
@@ -375,7 +471,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form):
|
class DeviceBayTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@@ -409,7 +505,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class DeviceForm(BootstrapMixin, CustomFieldForm):
|
class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect(
|
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, widget=APISelect(
|
||||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
display_field='display_name',
|
display_field='display_name',
|
||||||
attrs={'filter-for': 'position'}
|
attrs={'filter-for': 'position'}
|
||||||
@@ -446,7 +542,7 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
|||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
|
|
||||||
# Initialize helper selections
|
# Initialize helper selections
|
||||||
self.initial['site'] = self.instance.rack.site
|
self.initial['site'] = self.instance.site
|
||||||
self.initial['manufacturer'] = self.instance.device_type.manufacturer
|
self.initial['manufacturer'] = self.instance.device_type.manufacturer
|
||||||
|
|
||||||
# Compile list of choices for primary IPv4 and IPv6 addresses
|
# Compile list of choices for primary IPv4 and IPv6 addresses
|
||||||
@@ -513,7 +609,7 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
|||||||
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
||||||
self.fields['site'].disabled = True
|
self.fields['site'].disabled = True
|
||||||
self.fields['rack'].disabled = True
|
self.fields['rack'].disabled = True
|
||||||
self.initial['site'] = self.instance.parent_bay.device.rack.site_id
|
self.initial['site'] = self.instance.parent_bay.device.site_id
|
||||||
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
||||||
|
|
||||||
|
|
||||||
@@ -549,7 +645,7 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
|||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||||
'invalid_choice': 'Invalid site name.',
|
'invalid_choice': 'Invalid site name.',
|
||||||
})
|
})
|
||||||
rack_name = forms.CharField()
|
rack_name = forms.CharField(required=False)
|
||||||
face = forms.CharField(required=False)
|
face = forms.CharField(required=False)
|
||||||
|
|
||||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||||
@@ -635,18 +731,46 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
|
|
||||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Device
|
model = Device
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
|
q = forms.CharField(required=False, label='Search')
|
||||||
rack_group_id = FilterChoiceField(queryset=RackGroup.objects.annotate(filter_count=Count('racks__devices')),
|
site = FilterChoiceField(
|
||||||
label='Rack Group')
|
queryset=Site.objects.annotate(filter_count=Count('racks__devices')),
|
||||||
role = FilterChoiceField(queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), to_field_name='slug')
|
to_field_name='slug',
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
|
)
|
||||||
null_option=(0, 'None'))
|
rack_group_id = FilterChoiceField(
|
||||||
device_type_id = FilterChoiceField(queryset=DeviceType.objects.select_related('manufacturer')
|
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
|
||||||
.annotate(filter_count=Count('instances')), label='Type')
|
label='Rack group',
|
||||||
platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')),
|
)
|
||||||
to_field_name='slug', null_option=(0, 'None'))
|
role = FilterChoiceField(
|
||||||
status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
|
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
|
||||||
mac_address = forms.CharField(required=False, label='MAC address')
|
to_field_name='slug',
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
|
||||||
|
null_option=(0, 'None'),
|
||||||
|
)
|
||||||
|
manufacturer_id = FilterChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
label='Manufacturer',
|
||||||
|
)
|
||||||
|
device_type_id = FilterChoiceField(
|
||||||
|
queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
|
||||||
|
filter_count=Count('instances'),
|
||||||
|
),
|
||||||
|
label='Model',
|
||||||
|
)
|
||||||
|
platform = FilterChoiceField(
|
||||||
|
queryset=Platform.objects.annotate(filter_count=Count('devices')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None'),
|
||||||
|
)
|
||||||
|
status = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.Select(choices=FORM_STATUS_CHOICES),
|
||||||
|
)
|
||||||
|
mac_address = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='MAC address',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -679,14 +803,18 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortCreateForm(BootstrapMixin, forms.Form):
|
class ConsolePortCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionCSVForm(forms.Form):
|
class ConsoleConnectionCSVForm(forms.Form):
|
||||||
console_server = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_console_server=True),
|
console_server = FlexibleModelChoiceField(
|
||||||
to_field_name='name',
|
queryset=Device.objects.filter(device_type__is_console_server=True),
|
||||||
error_messages={'invalid_choice': 'Console server not found'})
|
to_field_name='name',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Console server not found',
|
||||||
|
}
|
||||||
|
)
|
||||||
cs_port = forms.CharField()
|
cs_port = forms.CharField()
|
||||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Device not found'})
|
error_messages={'invalid_choice': 'Device not found'})
|
||||||
@@ -751,22 +879,49 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
|
|||||||
|
|
||||||
|
|
||||||
class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
|
class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
site = forms.ModelChoiceField(
|
||||||
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
queryset=Site.objects.all(),
|
||||||
console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
|
widget=forms.HiddenInput(),
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
|
)
|
||||||
display_field='display_name',
|
rack = forms.ModelChoiceField(
|
||||||
attrs={'filter-for': 'cs_port'}))
|
queryset=Rack.objects.all(),
|
||||||
livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
|
label='Rack',
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
|
required=False,
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={'filter-for': 'console_server', 'nullable': 'true'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
console_server = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Console Server',
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_console_server=True',
|
||||||
|
display_field='display_name',
|
||||||
|
attrs={'filter-for': 'cs_port'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
livesearch = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Console Server',
|
||||||
|
widget=Livesearch(
|
||||||
|
query_key='q',
|
||||||
|
query_url='dcim-api:device_list',
|
||||||
|
field_to_update='console_server',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cs_port = forms.ModelChoiceField(
|
||||||
|
queryset=ConsoleServerPort.objects.all(),
|
||||||
|
label='Port',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
|
||||||
|
disabled_indicator='connected_console',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
cs_port = forms.ModelChoiceField(queryset=ConsoleServerPort.objects.all(), label='Port',
|
|
||||||
widget=APISelect(api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
|
|
||||||
disabled_indicator='connected_console'))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ['rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
|
fields = ['site', 'rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
|
||||||
labels = {
|
labels = {
|
||||||
'cs_port': 'Port',
|
'cs_port': 'Port',
|
||||||
'connection_status': 'Status',
|
'connection_status': 'Status',
|
||||||
@@ -779,17 +934,22 @@ class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
|
|||||||
if not self.instance.pk:
|
if not self.instance.pk:
|
||||||
raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
|
raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
|
||||||
|
|
||||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
|
self.initial['site'] = self.instance.device.site
|
||||||
|
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.site)
|
||||||
self.fields['cs_port'].required = True
|
self.fields['cs_port'].required = True
|
||||||
self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
|
self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
|
||||||
|
|
||||||
# Initialize console server choices
|
# Initialize console server choices
|
||||||
if self.is_bound and self.data.get('rack'):
|
if self.is_bound and self.data.get('rack'):
|
||||||
self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_console_server=True)
|
self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'],
|
||||||
|
device_type__is_console_server=True)
|
||||||
elif self.initial.get('rack'):
|
elif self.initial.get('rack'):
|
||||||
self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_console_server=True)
|
self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'],
|
||||||
|
device_type__is_console_server=True)
|
||||||
else:
|
else:
|
||||||
self.fields['console_server'].choices = []
|
self.fields['console_server'].queryset = Device.objects.filter(site=self.instance.device.site,
|
||||||
|
rack__isnull=True,
|
||||||
|
device_type__is_console_server=True)
|
||||||
|
|
||||||
# Initialize CS port choices
|
# Initialize CS port choices
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
@@ -814,27 +974,61 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
|
class ConsoleServerPortCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
||||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
site = forms.ModelChoiceField(
|
||||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
queryset=Site.objects.all(),
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
widget=forms.HiddenInput(),
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
)
|
||||||
display_field='display_name', attrs={'filter-for': 'port'}))
|
rack = forms.ModelChoiceField(
|
||||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
queryset=Rack.objects.all(),
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
label='Rack',
|
||||||
|
required=False,
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device',
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||||
|
display_field='display_name',
|
||||||
|
attrs={'filter-for': 'port'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
livesearch = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Device',
|
||||||
|
widget=Livesearch(
|
||||||
|
query_key='q',
|
||||||
|
query_url='dcim-api:device_list',
|
||||||
|
field_to_update='device'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
port = forms.ModelChoiceField(
|
||||||
|
queryset=ConsolePort.objects.all(),
|
||||||
|
label='Port',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/{{device}}/console-ports/',
|
||||||
|
disabled_indicator='cs_port'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connection_status = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
initial=CONNECTION_STATUS_CONNECTED,
|
||||||
|
label='Status',
|
||||||
|
widget=forms.Select(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES
|
||||||
|
)
|
||||||
)
|
)
|
||||||
port = forms.ModelChoiceField(queryset=ConsolePort.objects.all(), label='Port',
|
|
||||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/console-ports/',
|
|
||||||
disabled_indicator='cs_port'))
|
|
||||||
connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
|
|
||||||
widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
|
fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||||
labels = {
|
labels = {
|
||||||
'connection_status': 'Status',
|
'connection_status': 'Status',
|
||||||
}
|
}
|
||||||
@@ -843,7 +1037,8 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
|||||||
|
|
||||||
super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
|
super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.rack.site)
|
self.initial['site'] = consoleserverport.device.site
|
||||||
|
self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.site)
|
||||||
|
|
||||||
# Initialize device choices
|
# Initialize device choices
|
||||||
if self.is_bound and self.data.get('rack'):
|
if self.is_bound and self.data.get('rack'):
|
||||||
@@ -851,7 +1046,8 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
|||||||
elif self.initial.get('rack', None):
|
elif self.initial.get('rack', None):
|
||||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||||
else:
|
else:
|
||||||
self.fields['device'].choices = []
|
self.fields['device'].queryset = Device.objects.filter(site=consoleserverport.device.site,
|
||||||
|
rack__isnull=True)
|
||||||
|
|
||||||
# Initialize port choices
|
# Initialize port choices
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
@@ -876,13 +1072,18 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerPortCreateForm(BootstrapMixin, forms.Form):
|
class PowerPortCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionCSVForm(forms.Form):
|
class PowerConnectionCSVForm(forms.Form):
|
||||||
pdu = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_pdu=True), to_field_name='name',
|
pdu = FlexibleModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'PDU not found.'})
|
queryset=Device.objects.filter(device_type__is_pdu=True),
|
||||||
|
to_field_name='name',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'PDU not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
power_outlet = forms.CharField()
|
power_outlet = forms.CharField()
|
||||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Device not found'})
|
error_messages={'invalid_choice': 'Device not found'})
|
||||||
@@ -948,21 +1149,46 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
|
class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput())
|
||||||
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
rack = forms.ModelChoiceField(
|
||||||
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
queryset=Rack.objects.all(),
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
|
label='Rack',
|
||||||
display_field='display_name', attrs={'filter-for': 'power_outlet'}))
|
required=False,
|
||||||
livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
|
widget=forms.Select(
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
|
attrs={'filter-for': 'pdu', 'nullable': 'true'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pdu = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='PDU',
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_pdu=True',
|
||||||
|
display_field='display_name',
|
||||||
|
attrs={'filter-for': 'power_outlet'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
livesearch = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='PDU',
|
||||||
|
widget=Livesearch(
|
||||||
|
query_key='q',
|
||||||
|
query_url='dcim-api:device_list',
|
||||||
|
field_to_update='pdu'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
power_outlet = forms.ModelChoiceField(
|
||||||
|
queryset=PowerOutlet.objects.all(),
|
||||||
|
label='Outlet',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
|
||||||
|
disabled_indicator='connected_port'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
power_outlet = forms.ModelChoiceField(queryset=PowerOutlet.objects.all(), label='Outlet',
|
|
||||||
widget=APISelect(api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
|
|
||||||
disabled_indicator='connected_port'))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ['rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
|
fields = ['site', 'rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
|
||||||
labels = {
|
labels = {
|
||||||
'power_outlet': 'Outlet',
|
'power_outlet': 'Outlet',
|
||||||
'connection_status': 'Status',
|
'connection_status': 'Status',
|
||||||
@@ -975,17 +1201,22 @@ class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
|
|||||||
if not self.instance.pk:
|
if not self.instance.pk:
|
||||||
raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
|
raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
|
||||||
|
|
||||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
|
self.initial['site'] = self.instance.device.site
|
||||||
|
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.site)
|
||||||
self.fields['power_outlet'].required = True
|
self.fields['power_outlet'].required = True
|
||||||
self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
|
self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
|
||||||
|
|
||||||
# Initialize PDU choices
|
# Initialize PDU choices
|
||||||
if self.is_bound and self.data.get('rack'):
|
if self.is_bound and self.data.get('rack'):
|
||||||
self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_pdu=True)
|
self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'],
|
||||||
|
device_type__is_pdu=True)
|
||||||
elif self.initial.get('rack', None):
|
elif self.initial.get('rack', None):
|
||||||
self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_pdu=True)
|
self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'],
|
||||||
|
device_type__is_pdu=True)
|
||||||
else:
|
else:
|
||||||
self.fields['pdu'].choices = []
|
self.fields['pdu'].queryset = Device.objects.filter(site=self.instance.device.site,
|
||||||
|
rack__isnull=True,
|
||||||
|
device_type__is_pdu=True)
|
||||||
|
|
||||||
# Initialize power outlet choices
|
# Initialize power outlet choices
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
@@ -1010,27 +1241,61 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletCreateForm(BootstrapMixin, forms.Form):
|
class PowerOutletCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
||||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
site = forms.ModelChoiceField(
|
||||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
queryset=Site.objects.all(),
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
widget=forms.HiddenInput()
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
)
|
||||||
display_field='display_name', attrs={'filter-for': 'port'}))
|
rack = forms.ModelChoiceField(
|
||||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
queryset=Rack.objects.all(),
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
label='Rack',
|
||||||
|
required=False,
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device',
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||||
|
display_field='display_name',
|
||||||
|
attrs={'filter-for': 'port'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
livesearch = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Device',
|
||||||
|
widget=Livesearch(
|
||||||
|
query_key='q',
|
||||||
|
query_url='dcim-api:device_list',
|
||||||
|
field_to_update='device'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
port = forms.ModelChoiceField(
|
||||||
|
queryset=PowerPort.objects.all(),
|
||||||
|
label='Port',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/{{device}}/power-ports/',
|
||||||
|
disabled_indicator='power_outlet'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connection_status = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
initial=CONNECTION_STATUS_CONNECTED,
|
||||||
|
label='Status',
|
||||||
|
widget=forms.Select(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES
|
||||||
|
)
|
||||||
)
|
)
|
||||||
port = forms.ModelChoiceField(queryset=PowerPort.objects.all(), label='Port',
|
|
||||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/power-ports/',
|
|
||||||
disabled_indicator='power_outlet'))
|
|
||||||
connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
|
|
||||||
widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
|
fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||||
labels = {
|
labels = {
|
||||||
'connection_status': 'Status',
|
'connection_status': 'Status',
|
||||||
}
|
}
|
||||||
@@ -1039,7 +1304,8 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
|||||||
|
|
||||||
super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
|
super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.rack.site)
|
self.initial['site'] = poweroutlet.device.site
|
||||||
|
self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.site)
|
||||||
|
|
||||||
# Initialize device choices
|
# Initialize device choices
|
||||||
if self.is_bound and self.data.get('rack'):
|
if self.is_bound and self.data.get('rack'):
|
||||||
@@ -1047,7 +1313,8 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
|||||||
elif self.initial.get('rack', None):
|
elif self.initial.get('rack', None):
|
||||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||||
else:
|
else:
|
||||||
self.fields['device'].choices = []
|
self.fields['device'].queryset = Device.objects.filter(site=poweroutlet.device.site,
|
||||||
|
rack__isnull=True)
|
||||||
|
|
||||||
# Initialize port choices
|
# Initialize port choices
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
@@ -1066,27 +1333,65 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
fields = ['device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(InterfaceForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
# Limit LAG choices to interfaces belonging to this device
|
||||||
|
if self.is_bound:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
|
||||||
|
device_id=self.data['device'], form_factor=IFACE_FF_LAG
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
|
||||||
|
device=self.instance.device, form_factor=IFACE_FF_LAG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||||
|
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||||
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
||||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Limit LAG choices to interfaces belonging to this device
|
||||||
|
if self.device is not None:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
|
||||||
|
device=self.device, form_factor=IFACE_FF_LAG
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
|
||||||
|
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = ['description']
|
nullable_fields = ['lag', 'description']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Limit LAG choices to interfaces which belong to the parent device.
|
||||||
|
if self.initial.get('device'):
|
||||||
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
|
device=self.initial['device'], form_factor=IFACE_FF_LAG
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields['lag'].choices = []
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -1094,22 +1399,55 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||||
interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
|
interface_a = forms.ChoiceField(
|
||||||
site_b = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
choices=[],
|
||||||
widget=forms.Select(attrs={'filter-for': 'rack_b'}))
|
widget=SelectWithDisabled,
|
||||||
rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
label='Interface'
|
||||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site_b}}',
|
)
|
||||||
attrs={'filter-for': 'device_b'}))
|
site_b = forms.ModelChoiceField(
|
||||||
device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
queryset=Site.objects.all(),
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
label='Site',
|
||||||
display_field='display_name',
|
required=False,
|
||||||
attrs={'filter-for': 'interface_b'}))
|
widget=forms.Select(
|
||||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
attrs={'filter-for': 'rack_b'}
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
|
)
|
||||||
|
)
|
||||||
|
rack_b = forms.ModelChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
label='Rack',
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/racks/?site_id={{site_b}}',
|
||||||
|
attrs={'filter-for': 'device_b', 'nullable': 'true'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device_b = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device',
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/?site_id={{site_b}}&rack_id={{rack_b}}',
|
||||||
|
display_field='display_name',
|
||||||
|
attrs={'filter-for': 'interface_b'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
livesearch = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Device',
|
||||||
|
widget=Livesearch(
|
||||||
|
query_key='q',
|
||||||
|
query_url='dcim-api:device_list',
|
||||||
|
field_to_update='device_b'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
interface_b = forms.ModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
label='Interface',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
|
||||||
|
disabled_indicator='is_connected'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
interface_b = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
|
|
||||||
widget=APISelect(api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
|
|
||||||
disabled_indicator='is_connected'))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceConnection
|
model = InterfaceConnection
|
||||||
@@ -1120,37 +1458,36 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
|||||||
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Initialize interface A choices
|
# Initialize interface A choices
|
||||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL)\
|
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(
|
||||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
self.fields['interface_a'].choices = [
|
self.fields['interface_a'].choices = [
|
||||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
||||||
]
|
]
|
||||||
|
|
||||||
# Initialize rack_b choices if site_b is set
|
# Initialize rack_b choices if site_b is set
|
||||||
if self.is_bound and self.data.get('site_b'):
|
if self.initial.get('site_b'):
|
||||||
self.fields['rack_b'].queryset = Rack.objects.filter(site__pk=self.data['site_b'])
|
|
||||||
elif self.initial.get('site_b'):
|
|
||||||
self.fields['rack_b'].queryset = Rack.objects.filter(site=self.initial['site_b'])
|
self.fields['rack_b'].queryset = Rack.objects.filter(site=self.initial['site_b'])
|
||||||
else:
|
else:
|
||||||
self.fields['rack_b'].choices = []
|
self.fields['rack_b'].choices = []
|
||||||
|
|
||||||
# Initialize device_b choices if rack_b is set
|
# Initialize device_b choices if rack_b or site_b is set
|
||||||
if self.is_bound and self.data.get('rack_b'):
|
if self.initial.get('rack_b'):
|
||||||
self.fields['device_b'].queryset = Device.objects.filter(rack__pk=self.data['rack_b'])
|
|
||||||
elif self.initial.get('rack_b'):
|
|
||||||
self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
|
self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
|
||||||
|
elif self.initial.get('site_b'):
|
||||||
|
self.fields['device_b'].queryset = Device.objects.filter(site=self.initial['site_b'], rack__isnull=True)
|
||||||
else:
|
else:
|
||||||
self.fields['device_b'].choices = []
|
self.fields['device_b'].choices = []
|
||||||
|
|
||||||
# Initialize interface_b choices if device_b is set
|
# Initialize interface_b choices if device_b is set
|
||||||
if self.is_bound:
|
if self.initial.get('device_b'):
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b'])\
|
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
).select_related(
|
||||||
elif self.initial.get('device_b'):
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b'])\
|
)
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
|
||||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
|
||||||
else:
|
else:
|
||||||
device_b_interfaces = []
|
device_b_interfaces = []
|
||||||
self.fields['interface_b'].choices = [
|
self.fields['interface_b'].choices = [
|
||||||
@@ -1159,13 +1496,21 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionCSVForm(forms.Form):
|
class InterfaceConnectionCSVForm(forms.Form):
|
||||||
device_a = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
device_a = FlexibleModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Device A not found.'})
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'Device A not found.'}
|
||||||
|
)
|
||||||
interface_a = forms.CharField()
|
interface_a = forms.CharField()
|
||||||
device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
device_b = FlexibleModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Device B not found.'})
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'Device B not found.'}
|
||||||
|
)
|
||||||
interface_b = forms.CharField()
|
interface_b = forms.CharField()
|
||||||
status = forms.CharField(validators=[validate_connection_status])
|
status = forms.CharField(
|
||||||
|
validators=[validate_connection_status]
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
@@ -1260,7 +1605,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayCreateForm(BootstrapMixin, forms.Form):
|
class DeviceBayCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-01-06 16:56
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0024_site_add_contact_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='interface_ordering',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[1, b'Slot/position'], [2, b'Name (alphabetically)']], default=1),
|
||||||
|
),
|
||||||
|
]
|
||||||
33
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
33
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-16 18:43
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('dcim', '0025_devicetype_add_interface_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RackReservation',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('description', models.CharField(max_length=100)),
|
||||||
|
('rack', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack')),
|
||||||
|
('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['created'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
21
netbox/dcim/migrations/0027_device_add_site.py
Normal file
21
netbox/dcim/migrations/0027_device_add_site.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-16 21:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0026_add_rack_reservations'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
netbox/dcim/migrations/0028_device_copy_rack_to_site.py
Normal file
23
netbox/dcim/migrations/0028_device_copy_rack_to_site.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-16 21:23
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def copy_site_from_rack(apps, schema_editor):
|
||||||
|
Device = apps.get_model('dcim', 'Device')
|
||||||
|
for device in Device.objects.all():
|
||||||
|
device.site = device.rack.site
|
||||||
|
device.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0027_device_add_site'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(copy_site_from_rack),
|
||||||
|
]
|
||||||
26
netbox/dcim/migrations/0029_allow_rackless_devices.py
Normal file
26
netbox/dcim/migrations/0029_allow_rackless_devices.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-16 21:25
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0028_device_copy_rack_to_site'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='rack',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||||
|
),
|
||||||
|
]
|
||||||
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-27 19:55
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0029_allow_rackless_devices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='lag',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name=b'Parent LAG'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
]
|
||||||
38
netbox/dcim/migrations/0031_regions.py
Normal file
38
netbox/dcim/migrations/0031_regions.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-28 17:14
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0030_interface_add_lag'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Region',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=50, unique=True)),
|
||||||
|
('slug', models.SlugField(unique=True)),
|
||||||
|
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('level', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='site',
|
||||||
|
name='region',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
|
||||||
|
),
|
||||||
|
]
|
||||||
21
netbox/dcim/migrations/0032_device_increase_name_length.py
Normal file
21
netbox/dcim/migrations/0032_device_increase_name_length.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-03-02 15:09
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import utilities.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0031_regions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='name',
|
||||||
|
field=utilities.fields.NullableCharField(blank=True, max_length=64, null=True, unique=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.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 django.db.models import Count, Q, ObjectDoesNotExist
|
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
|
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
|
||||||
@@ -56,8 +61,16 @@ SUBDEVICE_ROLE_CHOICES = (
|
|||||||
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
IFACE_ORDERING_POSITION = 1
|
||||||
|
IFACE_ORDERING_NAME = 2
|
||||||
|
IFACE_ORDERING_CHOICES = [
|
||||||
|
[IFACE_ORDERING_POSITION, 'Slot/position'],
|
||||||
|
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
|
||||||
|
]
|
||||||
|
|
||||||
# Virtual
|
# Virtual
|
||||||
IFACE_FF_VIRTUAL = 0
|
IFACE_FF_VIRTUAL = 0
|
||||||
|
IFACE_FF_LAG = 200
|
||||||
# Ethernet
|
# Ethernet
|
||||||
IFACE_FF_100ME_FIXED = 800
|
IFACE_FF_100ME_FIXED = 800
|
||||||
IFACE_FF_1GE_FIXED = 1000
|
IFACE_FF_1GE_FIXED = 1000
|
||||||
@@ -96,6 +109,7 @@ IFACE_FF_CHOICES = [
|
|||||||
'Virtual interfaces',
|
'Virtual interfaces',
|
||||||
[
|
[
|
||||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||||
|
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -138,6 +152,7 @@ IFACE_FF_CHOICES = [
|
|||||||
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
|
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
|
||||||
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
||||||
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||||
|
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -157,6 +172,11 @@ IFACE_FF_CHOICES = [
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
VIRTUAL_IFACE_TYPES = [
|
||||||
|
IFACE_FF_VIRTUAL,
|
||||||
|
IFACE_FF_LAG,
|
||||||
|
]
|
||||||
|
|
||||||
STATUS_ACTIVE = True
|
STATUS_ACTIVE = True
|
||||||
STATUS_OFFLINE = False
|
STATUS_OFFLINE = False
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
@@ -182,46 +202,27 @@ RPC_CLIENT_CHOICES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Region(MPTTModel):
|
||||||
"""
|
"""
|
||||||
Attempt to match interface names by their slot/position identifiers and order according. Matching is done using the
|
Sites can be grouped within geographic Regions.
|
||||||
following pattern:
|
|
||||||
|
|
||||||
{a}/{b}/{c}:{d}
|
|
||||||
|
|
||||||
Interfaces are ordered first by field a, then b, then c, and finally d. Leading text (which typically indicates the
|
|
||||||
interface's type) is ignored. If any fields are not contained by an interface name, those fields are treated as
|
|
||||||
None. 'None' is ordered after all other values. For example:
|
|
||||||
|
|
||||||
et-0/0/0
|
|
||||||
et-0/0/1
|
|
||||||
et-0/1/0
|
|
||||||
xe-0/1/1:0
|
|
||||||
xe-0/1/1:1
|
|
||||||
xe-0/1/1:2
|
|
||||||
xe-0/1/1:3
|
|
||||||
et-0/1/2
|
|
||||||
...
|
|
||||||
et-0/1/9
|
|
||||||
et-0/1/10
|
|
||||||
et-0/1/11
|
|
||||||
et-1/0/0
|
|
||||||
et-1/0/1
|
|
||||||
...
|
|
||||||
vlan1
|
|
||||||
vlan10
|
|
||||||
|
|
||||||
:param queryset: The base queryset to be ordered
|
|
||||||
:param sql_col: Table and name of the SQL column which contains the interface name (ex: ''dcim_interface.name')
|
|
||||||
:param primary_ordering: A tuple of fields which take ordering precedence before the interface name (optional)
|
|
||||||
"""
|
"""
|
||||||
ordering = primary_ordering + ('_id1', '_id2', '_id3', '_id4')
|
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
|
||||||
return queryset.extra(select={
|
name = models.CharField(max_length=50, unique=True)
|
||||||
'_id1': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
slug = models.SlugField(unique=True)
|
||||||
'_id2': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
|
||||||
'_id3': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col),
|
class MPTTMeta:
|
||||||
'_id4': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col),
|
order_insertion_by = ['name']
|
||||||
}).order_by(*ordering)
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -234,6 +235,7 @@ class SiteManager(NaturalOrderByManager):
|
|||||||
return self.natural_order_by('name')
|
return self.natural_order_by('name')
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Site(CreatedUpdatedModel, CustomFieldModel):
|
class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
@@ -241,7 +243,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
"""
|
"""
|
||||||
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)
|
region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
|
tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
facility = models.CharField(max_length=50, blank=True)
|
facility = models.CharField(max_length=50, blank=True)
|
||||||
asn = ASNField(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)
|
||||||
@@ -257,7 +260,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -267,6 +270,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return csv_format([
|
return csv_format([
|
||||||
self.name,
|
self.name,
|
||||||
self.slug,
|
self.slug,
|
||||||
|
self.region.name if self.region else None,
|
||||||
self.tenant.name if self.tenant else None,
|
self.tenant.name if self.tenant else None,
|
||||||
self.facility,
|
self.facility,
|
||||||
self.asn,
|
self.asn,
|
||||||
@@ -300,6 +304,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class RackGroup(models.Model):
|
class RackGroup(models.Model):
|
||||||
"""
|
"""
|
||||||
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
|
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
|
||||||
@@ -317,13 +322,14 @@ class RackGroup(models.Model):
|
|||||||
['site', 'slug'],
|
['site', 'slug'],
|
||||||
]
|
]
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return u'{} - {}'.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)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class RackRole(models.Model):
|
class RackRole(models.Model):
|
||||||
"""
|
"""
|
||||||
Racks can be organized by functional role, similar to Devices.
|
Racks can be organized by functional role, similar to Devices.
|
||||||
@@ -335,7 +341,7 @@ class RackRole(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -348,6 +354,7 @@ class RackManager(NaturalOrderByManager):
|
|||||||
return self.natural_order_by('site__name', 'name')
|
return self.natural_order_by('site__name', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Rack(CreatedUpdatedModel, CustomFieldModel):
|
class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
@@ -378,7 +385,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
['site', 'facility_id'],
|
['site', 'facility_id'],
|
||||||
]
|
]
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.display_name
|
return self.display_name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -398,6 +405,19 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Record the original site assignment for this rack.
|
||||||
|
_site_id = None
|
||||||
|
if self.pk:
|
||||||
|
_site_id = Rack.objects.get(pk=self.pk).site_id
|
||||||
|
|
||||||
|
super(Rack, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Update racked devices if the assigned Site has been changed.
|
||||||
|
if _site_id is not None and self.site_id != _site_id:
|
||||||
|
Device.objects.filter(rack=self).update(site_id=self.site.pk)
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return csv_format([
|
return csv_format([
|
||||||
self.site.name,
|
self.site.name,
|
||||||
@@ -477,7 +497,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
|
devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
|
||||||
|
|
||||||
# Initialize the rack unit skeleton
|
# Initialize the rack unit skeleton
|
||||||
units = range(1, self.u_height + 1)
|
units = list(range(1, self.u_height + 1))
|
||||||
|
|
||||||
# Remove units consumed by installed devices
|
# Remove units consumed by installed devices
|
||||||
for d in devices:
|
for d in devices:
|
||||||
@@ -508,10 +528,55 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return int(float(self.u_height - u_available) / self.u_height * 100)
|
return int(float(self.u_height - u_available) / self.u_height * 100)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class RackReservation(models.Model):
|
||||||
|
"""
|
||||||
|
One or more reserved units within a Rack.
|
||||||
|
"""
|
||||||
|
rack = models.ForeignKey('Rack', related_name='reservations', editable=False, on_delete=models.CASCADE)
|
||||||
|
units = ArrayField(models.PositiveSmallIntegerField())
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT)
|
||||||
|
description = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['created']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return u"Reservation for rack {}".format(self.rack)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
if self.units:
|
||||||
|
|
||||||
|
# Validate that all specified units exist in the Rack.
|
||||||
|
invalid_units = [u for u in self.units if u not in self.rack.units]
|
||||||
|
if invalid_units:
|
||||||
|
raise ValidationError({
|
||||||
|
'units': u"Invalid unit(s) for {}U rack: {}".format(
|
||||||
|
self.rack.u_height,
|
||||||
|
', '.join([str(u) for u in invalid_units]),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check that none of the units has already been reserved for this Rack.
|
||||||
|
reserved_units = []
|
||||||
|
for resv in self.rack.reservations.exclude(pk=self.pk):
|
||||||
|
reserved_units += resv.units
|
||||||
|
conflicting_units = [u for u in self.units if u in reserved_units]
|
||||||
|
if conflicting_units:
|
||||||
|
raise ValidationError({
|
||||||
|
'units': 'The following units have already been reserved: {}'.format(
|
||||||
|
', '.join([str(u) for u in conflicting_units]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device Types
|
# Device Types
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Manufacturer(models.Model):
|
class Manufacturer(models.Model):
|
||||||
"""
|
"""
|
||||||
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
||||||
@@ -522,13 +587,14 @@ class Manufacturer(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
|
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class DeviceType(models.Model, CustomFieldModel):
|
class DeviceType(models.Model, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||||
@@ -551,6 +617,8 @@ class DeviceType(models.Model, CustomFieldModel):
|
|||||||
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")
|
||||||
|
interface_ordering = models.PositiveSmallIntegerField(choices=IFACE_ORDERING_CHOICES,
|
||||||
|
default=IFACE_ORDERING_POSITION)
|
||||||
is_console_server = models.BooleanField(default=False, verbose_name='Is a console server',
|
is_console_server = models.BooleanField(default=False, verbose_name='Is a console server',
|
||||||
help_text="This type of device has console server ports")
|
help_text="This type of device has console server ports")
|
||||||
is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU',
|
is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU',
|
||||||
@@ -571,7 +639,7 @@ class DeviceType(models.Model, CustomFieldModel):
|
|||||||
['manufacturer', 'slug'],
|
['manufacturer', 'slug'],
|
||||||
]
|
]
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.model
|
return self.model
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -641,6 +709,7 @@ class DeviceType(models.Model, CustomFieldModel):
|
|||||||
return bool(self.subdevice_role is False)
|
return bool(self.subdevice_role is False)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class ConsolePortTemplate(models.Model):
|
class ConsolePortTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A template for a ConsolePort to be created for a new Device.
|
A template for a ConsolePort to be created for a new Device.
|
||||||
@@ -652,10 +721,11 @@ class ConsolePortTemplate(models.Model):
|
|||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ['device_type', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class ConsoleServerPortTemplate(models.Model):
|
class ConsoleServerPortTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A template for a ConsoleServerPort to be created for a new Device.
|
A template for a ConsoleServerPort to be created for a new Device.
|
||||||
@@ -667,10 +737,11 @@ class ConsoleServerPortTemplate(models.Model):
|
|||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ['device_type', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class PowerPortTemplate(models.Model):
|
class PowerPortTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A template for a PowerPort to be created for a new Device.
|
A template for a PowerPort to be created for a new Device.
|
||||||
@@ -682,10 +753,11 @@ class PowerPortTemplate(models.Model):
|
|||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ['device_type', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class PowerOutletTemplate(models.Model):
|
class PowerOutletTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A template for a PowerOutlet to be created for a new Device.
|
A template for a PowerOutlet to be created for a new Device.
|
||||||
@@ -697,17 +769,49 @@ class PowerOutletTemplate(models.Model):
|
|||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ['device_type', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateManager(models.Manager):
|
class InterfaceManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def order_naturally(self, method=IFACE_ORDERING_POSITION):
|
||||||
qs = super(InterfaceTemplateManager, self).get_queryset()
|
"""
|
||||||
return order_interfaces(qs, 'dcim_interfacetemplate.name', ('device_type',))
|
Naturally order interfaces by their name and numeric position. The sort method must be one of the defined
|
||||||
|
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
|
||||||
|
|
||||||
|
To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
|
||||||
|
slot, subslot, position, and channel:
|
||||||
|
|
||||||
|
{name}{slot}/{subslot}/{position}:{channel}
|
||||||
|
|
||||||
|
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
||||||
|
be parsed as follows:
|
||||||
|
|
||||||
|
name = 'GigabitEthernet'
|
||||||
|
slot = None
|
||||||
|
subslot = 0
|
||||||
|
position = 1
|
||||||
|
channel = None
|
||||||
|
|
||||||
|
The chosen sorting method will determine which fields are ordered first in the query.
|
||||||
|
"""
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
sql_col = '{}.name'.format(queryset.model._meta.db_table)
|
||||||
|
ordering = {
|
||||||
|
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_name'),
|
||||||
|
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel'),
|
||||||
|
}[method]
|
||||||
|
return queryset.extra(select={
|
||||||
|
'_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||||
|
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
|
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
|
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
|
'_channel': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col),
|
||||||
|
}).order_by(*ordering)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class InterfaceTemplate(models.Model):
|
class InterfaceTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A template for a physical data interface on a new Device.
|
A template for a physical data interface on a new Device.
|
||||||
@@ -717,16 +821,17 @@ class InterfaceTemplate(models.Model):
|
|||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
||||||
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
||||||
|
|
||||||
objects = InterfaceTemplateManager()
|
objects = InterfaceManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ['device_type', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class DeviceBayTemplate(models.Model):
|
class DeviceBayTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A template for a DeviceBay to be created for a new parent Device.
|
A template for a DeviceBay to be created for a new parent Device.
|
||||||
@@ -738,7 +843,7 @@ class DeviceBayTemplate(models.Model):
|
|||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ['device_type', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@@ -746,6 +851,7 @@ class DeviceBayTemplate(models.Model):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class DeviceRole(models.Model):
|
class DeviceRole(models.Model):
|
||||||
"""
|
"""
|
||||||
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
||||||
@@ -758,13 +864,14 @@ class DeviceRole(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
|
return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Platform(models.Model):
|
class Platform(models.Model):
|
||||||
"""
|
"""
|
||||||
Platform refers to the software or firmware running on a Device; for example, "Cisco IOS-XR" or "Juniper Junos".
|
Platform refers to the software or firmware running on a Device; for example, "Cisco IOS-XR" or "Juniper Junos".
|
||||||
@@ -778,7 +885,7 @@ class Platform(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -791,6 +898,7 @@ class DeviceManager(NaturalOrderByManager):
|
|||||||
return self.natural_order_by('name')
|
return self.natural_order_by('name')
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Device(CreatedUpdatedModel, CustomFieldModel):
|
class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
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,
|
||||||
@@ -807,11 +915,12 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
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)
|
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=64, 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')
|
||||||
asset_tag = NullableCharField(max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
|
asset_tag = NullableCharField(max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
|
||||||
help_text='A unique tag used to identify this device')
|
help_text='A unique tag used to identify this device')
|
||||||
rack = models.ForeignKey('Rack', related_name='devices', on_delete=models.PROTECT)
|
site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT)
|
||||||
|
rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
|
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
|
||||||
verbose_name='Position (U)',
|
verbose_name='Position (U)',
|
||||||
help_text='The lowest-numbered unit occupied by the device')
|
help_text='The lowest-numbered unit occupied by the device')
|
||||||
@@ -830,7 +939,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
unique_together = ['rack', 'position', 'face']
|
unique_together = ['rack', 'position', 'face']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.display_name
|
return self.display_name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -838,41 +947,59 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
# Validate site/rack combination
|
||||||
|
if self.rack and self.site != self.rack.site:
|
||||||
|
raise ValidationError({
|
||||||
|
'rack': "Rack {} does not belong to site {}.".format(self.rack, self.site),
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.rack is None:
|
||||||
|
if self.face is not None:
|
||||||
|
raise ValidationError({
|
||||||
|
'face': "Cannot select a rack face without assigning a rack.",
|
||||||
|
})
|
||||||
|
if self.position:
|
||||||
|
raise ValidationError({
|
||||||
|
'face': "Cannot select a rack position without assigning a rack.",
|
||||||
|
})
|
||||||
|
|
||||||
# Validate position/face combination
|
# Validate position/face combination
|
||||||
if self.position and self.face is None:
|
if self.position and self.face is None:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'face': "Must specify rack face when defining rack position."
|
'face': "Must specify rack face when defining rack position.",
|
||||||
})
|
})
|
||||||
|
|
||||||
try:
|
if self.rack:
|
||||||
# Child devices cannot be assigned to a rack face/unit
|
|
||||||
if self.device_type.is_child_device and self.face is not None:
|
|
||||||
raise ValidationError({
|
|
||||||
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
|
|
||||||
"device."
|
|
||||||
})
|
|
||||||
if self.device_type.is_child_device and self.position:
|
|
||||||
raise ValidationError({
|
|
||||||
'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
|
|
||||||
"parent device."
|
|
||||||
})
|
|
||||||
|
|
||||||
# Validate rack space
|
|
||||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
|
||||||
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,
|
# Child devices cannot be assigned to a rack face/unit
|
||||||
exclude=exclude_list)
|
if self.device_type.is_child_device and self.face is not None:
|
||||||
if self.position and self.position not in available_units:
|
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
|
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
|
||||||
"({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
"device."
|
||||||
|
})
|
||||||
|
if self.device_type.is_child_device and self.position:
|
||||||
|
raise ValidationError({
|
||||||
|
'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
|
||||||
|
"parent device."
|
||||||
})
|
})
|
||||||
except Rack.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
except DeviceType.DoesNotExist:
|
# Validate rack space
|
||||||
pass
|
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||||
|
exclude_list = [self.pk] if self.pk else []
|
||||||
|
try:
|
||||||
|
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||||
|
exclude=exclude_list)
|
||||||
|
if self.position and self.position not in available_units:
|
||||||
|
raise ValidationError({
|
||||||
|
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
|
||||||
|
"({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
||||||
|
})
|
||||||
|
except Rack.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except DeviceType.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
@@ -920,8 +1047,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
self.platform.name if self.platform else None,
|
self.platform.name if self.platform else None,
|
||||||
self.serial,
|
self.serial,
|
||||||
self.asset_tag,
|
self.asset_tag,
|
||||||
self.rack.site.name,
|
self.site.name,
|
||||||
self.rack.name,
|
self.rack.name if self.rack else None,
|
||||||
self.position,
|
self.position,
|
||||||
self.get_face_display(),
|
self.get_face_display(),
|
||||||
])
|
])
|
||||||
@@ -970,6 +1097,11 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return RPC_CLIENTS.get(self.platform.rpc_client)
|
return RPC_CLIENTS.get(self.platform.rpc_client)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Console ports
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class ConsolePort(models.Model):
|
class ConsolePort(models.Model):
|
||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
@@ -984,12 +1116,9 @@ class ConsolePort(models.Model):
|
|||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_parent_url(self):
|
|
||||||
return self.device.get_absolute_url()
|
|
||||||
|
|
||||||
# Used for connections export
|
# Used for connections export
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return csv_format([
|
return csv_format([
|
||||||
@@ -1001,6 +1130,10 @@ class ConsolePort(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Console server ports
|
||||||
|
#
|
||||||
|
|
||||||
class ConsoleServerPortManager(models.Manager):
|
class ConsoleServerPortManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -1016,6 +1149,7 @@ class ConsoleServerPortManager(models.Manager):
|
|||||||
}).order_by('device', 'name_as_integer')
|
}).order_by('device', 'name_as_integer')
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class ConsoleServerPort(models.Model):
|
class ConsoleServerPort(models.Model):
|
||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||||
@@ -1028,13 +1162,15 @@ class ConsoleServerPort(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_parent_url(self):
|
|
||||||
return self.device.get_absolute_url()
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power ports
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class PowerPort(models.Model):
|
class PowerPort(models.Model):
|
||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
@@ -1049,15 +1185,12 @@ class PowerPort(models.Model):
|
|||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_parent_url(self):
|
|
||||||
return self.device.get_absolute_url()
|
|
||||||
|
|
||||||
# Used for connections export
|
# Used for connections export
|
||||||
def csv_format(self):
|
def to_csv(self):
|
||||||
return ','.join([
|
return csv_format([
|
||||||
self.power_outlet.device.identifier if self.power_outlet else None,
|
self.power_outlet.device.identifier if self.power_outlet else None,
|
||||||
self.power_outlet.name if self.power_outlet else None,
|
self.power_outlet.name if self.power_outlet else None,
|
||||||
self.device.identifier,
|
self.device.identifier,
|
||||||
@@ -1066,6 +1199,10 @@ class PowerPort(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power outlets
|
||||||
|
#
|
||||||
|
|
||||||
class PowerOutletManager(models.Manager):
|
class PowerOutletManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -1075,6 +1212,7 @@ class PowerOutletManager(models.Manager):
|
|||||||
}).order_by('device', 'name_padded')
|
}).order_by('device', 'name_padded')
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class PowerOutlet(models.Model):
|
class PowerOutlet(models.Model):
|
||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||||
@@ -1087,32 +1225,23 @@ class PowerOutlet(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_parent_url(self):
|
|
||||||
return self.device.get_absolute_url()
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceManager(models.Manager):
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
qs = super(InterfaceManager, self).get_queryset()
|
|
||||||
return order_interfaces(qs, 'dcim_interface.name', ('device',))
|
|
||||||
|
|
||||||
def virtual(self):
|
|
||||||
return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL)
|
|
||||||
|
|
||||||
def physical(self):
|
|
||||||
return self.get_queryset().exclude(form_factor=IFACE_FF_VIRTUAL)
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Interfaces
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Interface(models.Model):
|
class Interface(models.Model):
|
||||||
"""
|
"""
|
||||||
A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
|
A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
|
||||||
of an InterfaceConnection.
|
of an InterfaceConnection.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
||||||
|
lag = models.ForeignKey('self', related_name='member_interfaces', null=True, blank=True, on_delete=models.SET_NULL,
|
||||||
|
verbose_name='Parent LAG')
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
||||||
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
||||||
@@ -1126,23 +1255,47 @@ class Interface(models.Model):
|
|||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_parent_url(self):
|
|
||||||
return self.device.get_absolute_url()
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected:
|
# Virtual interfaces cannot be connected
|
||||||
|
if self.form_factor in VIRTUAL_IFACE_TYPES and self.is_connected:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
||||||
"interface or choose a physical form factor."
|
"interface or choose a physical form factor."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# An interface's LAG must belong to the same device
|
||||||
|
if self.lag and self.lag.device != self.device:
|
||||||
|
raise ValidationError({
|
||||||
|
'lag': u"The selected LAG interface ({}) belongs to a different device ({}).".format(
|
||||||
|
self.lag.name, self.lag.device.name
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
# A virtual interface cannot have a parent LAG
|
||||||
|
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
|
||||||
|
raise ValidationError({
|
||||||
|
'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
||||||
|
})
|
||||||
|
|
||||||
|
# Only a LAG can have LAG members
|
||||||
|
if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists():
|
||||||
|
raise ValidationError({
|
||||||
|
'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format(
|
||||||
|
u", ".join([iface.name for iface in self.member_interfaces.all()])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_physical(self):
|
def is_virtual(self):
|
||||||
return self.form_factor != IFACE_FF_VIRTUAL
|
return self.form_factor in VIRTUAL_IFACE_TYPES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_lag(self):
|
||||||
|
return self.form_factor == IFACE_FF_LAG
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
@@ -1206,6 +1359,11 @@ class InterfaceConnection(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device bays
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class DeviceBay(models.Model):
|
class DeviceBay(models.Model):
|
||||||
"""
|
"""
|
||||||
An empty space within a Device which can house a child device
|
An empty space within a Device which can house a child device
|
||||||
@@ -1219,12 +1377,9 @@ class DeviceBay(models.Model):
|
|||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return u'{} - {}'.format(self.device.name, self.name)
|
return u'{} - {}'.format(self.device.name, self.name)
|
||||||
|
|
||||||
def get_parent_url(self):
|
|
||||||
return self.device.get_absolute_url()
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Validate that the parent Device can have DeviceBays
|
# Validate that the parent Device can have DeviceBays
|
||||||
@@ -1238,6 +1393,11 @@ class DeviceBay(models.Model):
|
|||||||
raise ValidationError("Cannot install a device into itself.")
|
raise ValidationError("Cannot install a device into itself.")
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Modules
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Module(models.Model):
|
class Module(models.Model):
|
||||||
"""
|
"""
|
||||||
A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only
|
A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only
|
||||||
@@ -1256,8 +1416,5 @@ class Module(models.Model):
|
|||||||
ordering = ['device__id', 'parent__id', 'name']
|
ordering = ['device__id', 'parent__id', 'name']
|
||||||
unique_together = ['device', 'parent', 'name']
|
unique_together = ['device', 'parent', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_parent_url(self):
|
|
||||||
return reverse('dcim:device_inventory', args=[self.device.pk])
|
|
||||||
|
|||||||
@@ -6,10 +6,28 @@ from utilities.tables import BaseTable, ToggleColumn
|
|||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||||
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||||
RackGroup, Site,
|
RackGroup, Region, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
REGION_LINK = """
|
||||||
|
{% if record.get_children %}
|
||||||
|
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i></a>
|
||||||
|
{% else %}
|
||||||
|
<span style="padding-left: {{ record.get_ancestors|length }}9px">
|
||||||
|
{% endif %}
|
||||||
|
{{ record.name }}
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
|
||||||
|
SITE_REGION_LINK = """
|
||||||
|
{% if record.region %}
|
||||||
|
<a href="{% url 'dcim:site_list' %}?region={{ record.region.slug }}">{{ record.region }}</a>
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
COLOR_LABEL = """
|
COLOR_LABEL = """
|
||||||
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
|
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
|
||||||
"""
|
"""
|
||||||
@@ -20,6 +38,12 @@ DEVICE_LINK = """
|
|||||||
</a>
|
</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
REGION_ACTIONS = """
|
||||||
|
{% if perms.dcim.change_region %}
|
||||||
|
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
RACKGROUP_ACTIONS = """
|
RACKGROUP_ACTIONS = """
|
||||||
{% if perms.dcim.change_rackgroup %}
|
{% if perms.dcim.change_rackgroup %}
|
||||||
<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>
|
<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>
|
||||||
@@ -70,12 +94,38 @@ STATUS_ICON = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DEVICE_PRIMARY_IP = """
|
||||||
|
{{ record.primary_ip6.address.ip|default:"" }}
|
||||||
|
{% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
|
||||||
|
{{ record.primary_ip4.address.ip|default:"" }}
|
||||||
|
"""
|
||||||
|
|
||||||
UTILIZATION_GRAPH = """
|
UTILIZATION_GRAPH = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% utilization_graph value %}
|
{% utilization_graph value %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
class RegionTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False)
|
||||||
|
site_count = tables.Column(verbose_name='Sites')
|
||||||
|
slug = tables.Column(verbose_name='Slug')
|
||||||
|
actions = tables.TemplateColumn(
|
||||||
|
template_code=REGION_ACTIONS,
|
||||||
|
attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name=''
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Region
|
||||||
|
fields = ('pk', 'name', 'site_count', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
@@ -84,6 +134,7 @@ class SiteTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
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')
|
||||||
|
region = tables.TemplateColumn(template_code=SITE_REGION_LINK, verbose_name='Region')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
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')
|
||||||
@@ -94,8 +145,10 @@ class SiteTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Site
|
model = Site
|
||||||
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
fields = (
|
||||||
'vlan_count', 'circuit_count')
|
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||||
|
'vlan_count', 'circuit_count',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -311,13 +364,13 @@ class DeviceTable(BaseTable):
|
|||||||
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')
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], 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.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||||
device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||||
text=lambda record: record.device_type.full_name)
|
text=lambda record: record.device_type.full_name)
|
||||||
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
|
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
|
||||||
template_code="{{ record.primary_ip.address.ip }}")
|
template_code=DEVICE_PRIMARY_IP)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Device
|
model = Device
|
||||||
@@ -327,7 +380,7 @@ class DeviceTable(BaseTable):
|
|||||||
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')
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], 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')
|
||||||
device_role = tables.Column(verbose_name='Role')
|
device_role = tables.Column(verbose_name='Role')
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class SiteTest(APITestCase):
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'slug',
|
'slug',
|
||||||
|
'region',
|
||||||
'tenant',
|
'tenant',
|
||||||
'facility',
|
'facility',
|
||||||
'asn',
|
'asn',
|
||||||
@@ -65,7 +66,7 @@ class SiteTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -75,7 +76,7 @@ class SiteTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)):
|
def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -84,9 +85,9 @@ class SiteTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)):
|
def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in json.loads(response.content):
|
for i in json.loads(response.content.decode('utf-8')):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(i.keys()),
|
sorted(i.keys()),
|
||||||
sorted(self.rack_fields),
|
sorted(self.rack_fields),
|
||||||
@@ -99,9 +100,9 @@ class SiteTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)):
|
def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in json.loads(response.content):
|
for i in json.loads(response.content.decode('utf-8')):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(i.keys()),
|
sorted(i.keys()),
|
||||||
sorted(self.graph_fields),
|
sorted(self.graph_fields),
|
||||||
@@ -151,6 +152,7 @@ class RackTest(APITestCase):
|
|||||||
'width',
|
'width',
|
||||||
'u_height',
|
'u_height',
|
||||||
'desc_units',
|
'desc_units',
|
||||||
|
'reservations',
|
||||||
'comments',
|
'comments',
|
||||||
'custom_fields',
|
'custom_fields',
|
||||||
'front_units',
|
'front_units',
|
||||||
@@ -159,7 +161,7 @@ class RackTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -173,7 +175,7 @@ class RackTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)):
|
def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -202,7 +204,7 @@ class ManufacturersTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -212,7 +214,7 @@ class ManufacturersTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)):
|
def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -232,12 +234,14 @@ class DeviceTypeTest(APITestCase):
|
|||||||
'part_number',
|
'part_number',
|
||||||
'u_height',
|
'u_height',
|
||||||
'is_full_depth',
|
'is_full_depth',
|
||||||
|
'interface_ordering',
|
||||||
'is_console_server',
|
'is_console_server',
|
||||||
'is_pdu',
|
'is_pdu',
|
||||||
'is_network_device',
|
'is_network_device',
|
||||||
'subdevice_role',
|
'subdevice_role',
|
||||||
'comments',
|
'comments',
|
||||||
'custom_fields',
|
'custom_fields',
|
||||||
|
'instance_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
nested_fields = [
|
nested_fields = [
|
||||||
@@ -249,7 +253,7 @@ class DeviceTypeTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -260,7 +264,7 @@ class DeviceTypeTest(APITestCase):
|
|||||||
def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)):
|
def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)):
|
||||||
# TODO: details returns list view.
|
# TODO: details returns list view.
|
||||||
# response = self.client.get(endpoint)
|
# response = self.client.get(endpoint)
|
||||||
# content = json.loads(response.content)
|
# content = json.loads(response.content.decode('utf-8'))
|
||||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
# self.assertEqual(
|
# self.assertEqual(
|
||||||
# sorted(content.keys()),
|
# sorted(content.keys()),
|
||||||
@@ -283,7 +287,7 @@ class DeviceRolesTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -293,7 +297,7 @@ class DeviceRolesTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)):
|
def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -311,7 +315,7 @@ class PlatformsTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -321,7 +325,7 @@ class PlatformsTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)):
|
def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -343,6 +347,7 @@ class DeviceTest(APITestCase):
|
|||||||
'platform',
|
'platform',
|
||||||
'serial',
|
'serial',
|
||||||
'asset_tag',
|
'asset_tag',
|
||||||
|
'site',
|
||||||
'rack',
|
'rack',
|
||||||
'position',
|
'position',
|
||||||
'face',
|
'face',
|
||||||
@@ -359,7 +364,7 @@ class DeviceTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for device in content:
|
for device in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -414,6 +419,9 @@ class DeviceTest(APITestCase):
|
|||||||
'primary_ip4_family',
|
'primary_ip4_family',
|
||||||
'primary_ip4_id',
|
'primary_ip4_id',
|
||||||
'primary_ip6',
|
'primary_ip6',
|
||||||
|
'site_id',
|
||||||
|
'site_name',
|
||||||
|
'site_slug',
|
||||||
'rack_display_name',
|
'rack_display_name',
|
||||||
'rack_facility_id',
|
'rack_facility_id',
|
||||||
'rack_id',
|
'rack_id',
|
||||||
@@ -424,7 +432,7 @@ class DeviceTest(APITestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
device = content[0]
|
device = content[0]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -434,7 +442,7 @@ class DeviceTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)):
|
def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -452,7 +460,7 @@ class ConsoleServerPortsTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for console_port in content:
|
for console_port in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -474,7 +482,7 @@ class ConsolePortsTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for console_port in content:
|
for console_port in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -492,7 +500,7 @@ class ConsolePortsTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)):
|
def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -513,7 +521,7 @@ class PowerPortsTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -527,7 +535,7 @@ class PowerPortsTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)):
|
def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -548,7 +556,7 @@ class PowerOutletsTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -569,6 +577,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'device',
|
'device',
|
||||||
'name',
|
'name',
|
||||||
'form_factor',
|
'form_factor',
|
||||||
|
'lag',
|
||||||
'mac_address',
|
'mac_address',
|
||||||
'mgmt_only',
|
'mgmt_only',
|
||||||
'description',
|
'description',
|
||||||
@@ -582,6 +591,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'device',
|
'device',
|
||||||
'name',
|
'name',
|
||||||
'form_factor',
|
'form_factor',
|
||||||
|
'lag',
|
||||||
'mac_address',
|
'mac_address',
|
||||||
'mgmt_only',
|
'mgmt_only',
|
||||||
'description',
|
'description',
|
||||||
@@ -598,7 +608,7 @@ class InterfaceTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)):
|
def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -612,7 +622,7 @@ class InterfaceTest(APITestCase):
|
|||||||
|
|
||||||
def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)):
|
def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -624,19 +634,19 @@ class InterfaceTest(APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_get_graph_list(self, endpoint='/{}api/dcim/interfaces/1/graphs/'.format(settings.BASE_PATH)):
|
def test_get_graph_list(self, endpoint='/{}api/dcim/interfaces/1/graphs/'.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
for i in content:
|
for i in content:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(i.keys()),
|
sorted(i.keys()),
|
||||||
sorted(SiteTest.graph_fields),
|
sorted(SiteTest.graph_fields),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/'
|
def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/'
|
||||||
.format(settings.BASE_PATH)):
|
.format(settings.BASE_PATH)):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
@@ -658,7 +668,7 @@ class RelatedConnectionsTest(APITestCase):
|
|||||||
def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3'
|
def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3'
|
||||||
.format(settings.BASE_PATH))):
|
.format(settings.BASE_PATH))):
|
||||||
response = self.client.get(endpoint)
|
response = self.client.get(endpoint)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content.decode('utf-8'))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(content.keys()),
|
sorted(content.keys()),
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ class RackTestCase(TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
site = Site.objects.create(
|
self.site = Site.objects.create(
|
||||||
name='TestSite1',
|
name='TestSite1',
|
||||||
slug='my-test-site'
|
slug='my-test-site'
|
||||||
)
|
)
|
||||||
self.rack = Rack.objects.create(
|
self.rack = Rack.objects.create(
|
||||||
name='TestRack1',
|
name='TestRack1',
|
||||||
facility_id='A101',
|
facility_id='A101',
|
||||||
site=site,
|
site=self.site,
|
||||||
u_height=42
|
u_height=42
|
||||||
)
|
)
|
||||||
self.manufacturer = Manufacturer.objects.create(
|
self.manufacturer = Manufacturer.objects.create(
|
||||||
@@ -56,29 +56,29 @@ class RackTestCase(TestCase):
|
|||||||
|
|
||||||
def test_mount_single_device(self):
|
def test_mount_single_device(self):
|
||||||
|
|
||||||
rack1 = Rack.objects.get(name='TestRack1')
|
|
||||||
device1 = Device(
|
device1 = Device(
|
||||||
name='TestSwitch1',
|
name='TestSwitch1',
|
||||||
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
|
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
|
||||||
device_role=DeviceRole.objects.get(slug='switch'),
|
device_role=DeviceRole.objects.get(slug='switch'),
|
||||||
rack=rack1,
|
site=self.site,
|
||||||
|
rack=self.rack,
|
||||||
position=10,
|
position=10,
|
||||||
face=RACK_FACE_REAR,
|
face=RACK_FACE_REAR,
|
||||||
)
|
)
|
||||||
device1.save()
|
device1.save()
|
||||||
|
|
||||||
# Validate rack height
|
# Validate rack height
|
||||||
self.assertEqual(list(rack1.units), list(reversed(range(1, 43))))
|
self.assertEqual(list(self.rack.units), list(reversed(range(1, 43))))
|
||||||
|
|
||||||
# Validate inventory (front face)
|
# Validate inventory (front face)
|
||||||
rack1_inventory_front = rack1.get_front_elevation()
|
rack1_inventory_front = self.rack.get_front_elevation()
|
||||||
self.assertEqual(rack1_inventory_front[-10]['device'], device1)
|
self.assertEqual(rack1_inventory_front[-10]['device'], device1)
|
||||||
del(rack1_inventory_front[-10])
|
del(rack1_inventory_front[-10])
|
||||||
for u in rack1_inventory_front:
|
for u in rack1_inventory_front:
|
||||||
self.assertIsNone(u['device'])
|
self.assertIsNone(u['device'])
|
||||||
|
|
||||||
# Validate inventory (rear face)
|
# Validate inventory (rear face)
|
||||||
rack1_inventory_rear = rack1.get_rear_elevation()
|
rack1_inventory_rear = self.rack.get_rear_elevation()
|
||||||
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
|
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
|
||||||
del(rack1_inventory_rear[-10])
|
del(rack1_inventory_rear[-10])
|
||||||
for u in rack1_inventory_rear:
|
for u in rack1_inventory_rear:
|
||||||
@@ -89,6 +89,7 @@ class RackTestCase(TestCase):
|
|||||||
name='TestPDU',
|
name='TestPDU',
|
||||||
device_role=self.role.get('PDU'),
|
device_role=self.role.get('PDU'),
|
||||||
device_type=self.device_type.get('cc5000'),
|
device_type=self.device_type.get('cc5000'),
|
||||||
|
site=self.site,
|
||||||
rack=self.rack,
|
rack=self.rack,
|
||||||
position=None,
|
position=None,
|
||||||
face=None,
|
face=None,
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ from . import views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
# Regions
|
||||||
|
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
|
||||||
|
url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'),
|
||||||
|
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||||
|
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
|
||||||
|
|
||||||
# Sites
|
# Sites
|
||||||
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'),
|
||||||
@@ -29,6 +35,10 @@ urlpatterns = [
|
|||||||
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||||
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||||
|
|
||||||
|
# Rack reservations
|
||||||
|
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||||
|
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||||
|
|
||||||
# Racks
|
# Racks
|
||||||
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
||||||
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
|
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
|
||||||
@@ -38,6 +48,7 @@ urlpatterns = [
|
|||||||
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
|
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
|
||||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
||||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||||
|
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
|
||||||
|
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from .models import (
|
|||||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
RackRole, Site,
|
RackReservation, RackRole, Region, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -66,19 +66,20 @@ class ComponentCreateView(View):
|
|||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
|
|
||||||
parent = get_object_or_404(self.parent_model, pk=pk)
|
parent = get_object_or_404(self.parent_model, pk=pk)
|
||||||
|
form = self.form(parent, initial=request.GET)
|
||||||
|
|
||||||
return render(request, 'dcim/device_component_add.html', {
|
return render(request, 'dcim/device_component_add.html', {
|
||||||
'parent': parent,
|
'parent': parent,
|
||||||
'component_type': self.model._meta.verbose_name,
|
'component_type': self.model._meta.verbose_name,
|
||||||
'form': self.form(initial=request.GET),
|
'form': form,
|
||||||
'cancel_url': parent.get_absolute_url(),
|
'return_url': parent.get_absolute_url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
|
|
||||||
parent = get_object_or_404(self.parent_model, pk=pk)
|
parent = get_object_or_404(self.parent_model, pk=pk)
|
||||||
|
|
||||||
form = self.form(request.POST)
|
form = self.form(parent, request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
new_components = []
|
new_components = []
|
||||||
@@ -112,26 +113,62 @@ class ComponentCreateView(View):
|
|||||||
'parent': parent,
|
'parent': parent,
|
||||||
'component_type': self.model._meta.verbose_name,
|
'component_type': self.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': parent.get_absolute_url(),
|
'return_url': parent.get_absolute_url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentEditView(ObjectEditView):
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return obj.device.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentDeleteView(ObjectDeleteView):
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return obj.device.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Regions
|
||||||
|
#
|
||||||
|
|
||||||
|
class RegionListView(ObjectListView):
|
||||||
|
queryset = Region.objects.annotate(site_count=Count('sites'))
|
||||||
|
table = tables.RegionTable
|
||||||
|
template_name = 'dcim/region_list.html'
|
||||||
|
|
||||||
|
|
||||||
|
class RegionEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'dcim.change_region'
|
||||||
|
model = Region
|
||||||
|
form_class = forms.RegionForm
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return reverse('dcim:region_list')
|
||||||
|
|
||||||
|
|
||||||
|
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_region'
|
||||||
|
cls = Region
|
||||||
|
default_return_url = 'dcim:region_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteListView(ObjectListView):
|
class SiteListView(ObjectListView):
|
||||||
queryset = Site.objects.select_related('tenant')
|
queryset = Site.objects.select_related('region', 'tenant')
|
||||||
filter = filters.SiteFilter
|
filter = filters.SiteFilter
|
||||||
filter_form = forms.SiteFilterForm
|
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'
|
||||||
|
|
||||||
|
|
||||||
def site(request, slug):
|
def site(request, slug):
|
||||||
|
|
||||||
site = get_object_or_404(Site, slug=slug)
|
site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
|
||||||
stats = {
|
stats = {
|
||||||
'rack_count': Rack.objects.filter(site=site).count(),
|
'rack_count': Rack.objects.filter(site=site).count(),
|
||||||
'device_count': Device.objects.filter(rack__site=site).count(),
|
'device_count': Device.objects.filter(rack__site=site).count(),
|
||||||
@@ -157,13 +194,13 @@ class SiteEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model = Site
|
model = Site
|
||||||
form_class = forms.SiteForm
|
form_class = forms.SiteForm
|
||||||
template_name = 'dcim/site_edit.html'
|
template_name = 'dcim/site_edit.html'
|
||||||
obj_list_url = 'dcim:site_list'
|
default_return_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_site'
|
permission_required = 'dcim.delete_site'
|
||||||
model = Site
|
model = Site
|
||||||
redirect_url = 'dcim:site_list'
|
default_return_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -171,15 +208,16 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.SiteImportForm
|
form = forms.SiteImportForm
|
||||||
table = tables.SiteTable
|
table = tables.SiteTable
|
||||||
template_name = 'dcim/site_import.html'
|
template_name = 'dcim/site_import.html'
|
||||||
obj_list_url = 'dcim:site_list'
|
default_return_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_site'
|
permission_required = 'dcim.change_site'
|
||||||
cls = Site
|
cls = Site
|
||||||
|
filter = filters.SiteFilter
|
||||||
form = forms.SiteBulkEditForm
|
form = forms.SiteBulkEditForm
|
||||||
template_name = 'dcim/site_bulk_edit.html'
|
template_name = 'dcim/site_bulk_edit.html'
|
||||||
default_redirect_url = 'dcim:site_list'
|
default_return_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -191,7 +229,6 @@ class RackGroupListView(ObjectListView):
|
|||||||
filter = filters.RackGroupFilter
|
filter = filters.RackGroupFilter
|
||||||
filter_form = forms.RackGroupFilterForm
|
filter_form = forms.RackGroupFilterForm
|
||||||
table = tables.RackGroupTable
|
table = tables.RackGroupTable
|
||||||
edit_permissions = ['dcim.change_rackgroup', 'dcim.delete_rackgroup']
|
|
||||||
template_name = 'dcim/rackgroup_list.html'
|
template_name = 'dcim/rackgroup_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -199,14 +236,16 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'dcim.change_rackgroup'
|
permission_required = 'dcim.change_rackgroup'
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
form_class = forms.RackGroupForm
|
form_class = forms.RackGroupForm
|
||||||
obj_list_url = 'dcim:rackgroup_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('dcim:rackgroup_list')
|
||||||
|
|
||||||
|
|
||||||
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_rackgroup'
|
permission_required = 'dcim.delete_rackgroup'
|
||||||
cls = RackGroup
|
cls = RackGroup
|
||||||
default_redirect_url = 'dcim:rackgroup_list'
|
filter = filters.RackGroupFilter
|
||||||
|
default_return_url = 'dcim:rackgroup_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -216,7 +255,6 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
class RackRoleListView(ObjectListView):
|
class RackRoleListView(ObjectListView):
|
||||||
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
|
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
|
||||||
table = tables.RackRoleTable
|
table = tables.RackRoleTable
|
||||||
edit_permissions = ['dcim.change_rackrole', 'dcim.delete_rackrole']
|
|
||||||
template_name = 'dcim/rackrole_list.html'
|
template_name = 'dcim/rackrole_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -224,14 +262,15 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'dcim.change_rackrole'
|
permission_required = 'dcim.change_rackrole'
|
||||||
model = RackRole
|
model = RackRole
|
||||||
form_class = forms.RackRoleForm
|
form_class = forms.RackRoleForm
|
||||||
obj_list_url = 'dcim:rackrole_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('dcim:rackrole_list')
|
||||||
|
|
||||||
|
|
||||||
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_rackrole'
|
permission_required = 'dcim.delete_rackrole'
|
||||||
cls = RackRole
|
cls = RackRole
|
||||||
default_redirect_url = 'dcim:rackrole_list'
|
default_return_url = 'dcim:rackrole_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -244,21 +283,28 @@ class RackListView(ObjectListView):
|
|||||||
filter = filters.RackFilter
|
filter = filters.RackFilter
|
||||||
filter_form = forms.RackFilterForm
|
filter_form = forms.RackFilterForm
|
||||||
table = tables.RackTable
|
table = tables.RackTable
|
||||||
edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
|
|
||||||
template_name = 'dcim/rack_list.html'
|
template_name = 'dcim/rack_list.html'
|
||||||
|
|
||||||
|
|
||||||
def rack(request, pk):
|
def rack(request, pk):
|
||||||
|
|
||||||
rack = get_object_or_404(Rack, pk=pk)
|
rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
|
||||||
|
|
||||||
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__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()
|
||||||
|
|
||||||
|
reservations = RackReservation.objects.filter(rack=rack)
|
||||||
|
reserved_units = {}
|
||||||
|
for r in reservations:
|
||||||
|
for u in r.units:
|
||||||
|
reserved_units[u] = r
|
||||||
|
|
||||||
return render(request, 'dcim/rack.html', {
|
return render(request, 'dcim/rack.html', {
|
||||||
'rack': rack,
|
'rack': rack,
|
||||||
|
'reservations': reservations,
|
||||||
|
'reserved_units': reserved_units,
|
||||||
'nonracked_devices': nonracked_devices,
|
'nonracked_devices': nonracked_devices,
|
||||||
'next_rack': next_rack,
|
'next_rack': next_rack,
|
||||||
'prev_rack': prev_rack,
|
'prev_rack': prev_rack,
|
||||||
@@ -272,13 +318,13 @@ class RackEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model = Rack
|
model = Rack
|
||||||
form_class = forms.RackForm
|
form_class = forms.RackForm
|
||||||
template_name = 'dcim/rack_edit.html'
|
template_name = 'dcim/rack_edit.html'
|
||||||
obj_list_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_rack'
|
permission_required = 'dcim.delete_rack'
|
||||||
model = Rack
|
model = Rack
|
||||||
redirect_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -286,21 +332,50 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.RackImportForm
|
form = forms.RackImportForm
|
||||||
table = tables.RackImportTable
|
table = tables.RackImportTable
|
||||||
template_name = 'dcim/rack_import.html'
|
template_name = 'dcim/rack_import.html'
|
||||||
obj_list_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_rack'
|
permission_required = 'dcim.change_rack'
|
||||||
cls = Rack
|
cls = Rack
|
||||||
|
filter = filters.RackFilter
|
||||||
form = forms.RackBulkEditForm
|
form = forms.RackBulkEditForm
|
||||||
template_name = 'dcim/rack_bulk_edit.html'
|
template_name = 'dcim/rack_bulk_edit.html'
|
||||||
default_redirect_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_rack'
|
permission_required = 'dcim.delete_rack'
|
||||||
cls = Rack
|
cls = Rack
|
||||||
default_redirect_url = 'dcim:rack_list'
|
filter = filters.RackFilter
|
||||||
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'dcim.change_rackreservation'
|
||||||
|
model = RackReservation
|
||||||
|
form_class = forms.RackReservationForm
|
||||||
|
|
||||||
|
def alter_obj(self, obj, request, args, kwargs):
|
||||||
|
if not obj.pk:
|
||||||
|
obj.rack = get_object_or_404(Rack, pk=kwargs['rack'])
|
||||||
|
obj.user = request.user
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return obj.rack.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_rackreservation'
|
||||||
|
model = RackReservation
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return obj.rack.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -310,7 +385,6 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
class ManufacturerListView(ObjectListView):
|
class ManufacturerListView(ObjectListView):
|
||||||
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
||||||
table = tables.ManufacturerTable
|
table = tables.ManufacturerTable
|
||||||
edit_permissions = ['dcim.change_manufacturer', 'dcim.delete_manufacturer']
|
|
||||||
template_name = 'dcim/manufacturer_list.html'
|
template_name = 'dcim/manufacturer_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -318,14 +392,15 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'dcim.change_manufacturer'
|
permission_required = 'dcim.change_manufacturer'
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
form_class = forms.ManufacturerForm
|
form_class = forms.ManufacturerForm
|
||||||
obj_list_url = 'dcim:manufacturer_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('dcim:manufacturer_list')
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_manufacturer'
|
permission_required = 'dcim.delete_manufacturer'
|
||||||
cls = Manufacturer
|
cls = Manufacturer
|
||||||
default_redirect_url = 'dcim:manufacturer_list'
|
default_return_url = 'dcim:manufacturer_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -337,7 +412,6 @@ class DeviceTypeListView(ObjectListView):
|
|||||||
filter = filters.DeviceTypeFilter
|
filter = filters.DeviceTypeFilter
|
||||||
filter_form = forms.DeviceTypeFilterForm
|
filter_form = forms.DeviceTypeFilterForm
|
||||||
table = tables.DeviceTypeTable
|
table = tables.DeviceTypeTable
|
||||||
edit_permissions = ['dcim.change_devicetype', 'dcim.delete_devicetype']
|
|
||||||
template_name = 'dcim/devicetype_list.html'
|
template_name = 'dcim/devicetype_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -358,10 +432,14 @@ def devicetype(request, pk):
|
|||||||
poweroutlet_table = tables.PowerOutletTemplateTable(
|
poweroutlet_table = tables.PowerOutletTemplateTable(
|
||||||
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
)
|
)
|
||||||
mgmt_interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
|
mgmt_interface_table = tables.InterfaceTemplateTable(
|
||||||
mgmt_only=True))
|
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
|
||||||
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
|
mgmt_only=True))
|
||||||
mgmt_only=False))
|
)
|
||||||
|
interface_table = tables.InterfaceTemplateTable(
|
||||||
|
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
|
||||||
|
mgmt_only=False))
|
||||||
|
)
|
||||||
devicebay_table = tables.DeviceBayTemplateTable(
|
devicebay_table = tables.DeviceBayTemplateTable(
|
||||||
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
)
|
)
|
||||||
@@ -391,27 +469,29 @@ class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model = DeviceType
|
model = DeviceType
|
||||||
form_class = forms.DeviceTypeForm
|
form_class = forms.DeviceTypeForm
|
||||||
template_name = 'dcim/devicetype_edit.html'
|
template_name = 'dcim/devicetype_edit.html'
|
||||||
obj_list_url = 'dcim:devicetype_list'
|
default_return_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_devicetype'
|
permission_required = 'dcim.delete_devicetype'
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
redirect_url = 'dcim:devicetype_list'
|
default_return_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_devicetype'
|
permission_required = 'dcim.change_devicetype'
|
||||||
cls = DeviceType
|
cls = DeviceType
|
||||||
|
filter = filters.DeviceTypeFilter
|
||||||
form = forms.DeviceTypeBulkEditForm
|
form = forms.DeviceTypeBulkEditForm
|
||||||
template_name = 'dcim/devicetype_bulk_edit.html'
|
template_name = 'dcim/devicetype_bulk_edit.html'
|
||||||
default_redirect_url = 'dcim:devicetype_list'
|
default_return_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_devicetype'
|
permission_required = 'dcim.delete_devicetype'
|
||||||
cls = DeviceType
|
cls = DeviceType
|
||||||
default_redirect_url = 'dcim:devicetype_list'
|
filter = filters.DeviceTypeFilter
|
||||||
|
default_return_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -525,7 +605,6 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
class DeviceRoleListView(ObjectListView):
|
class DeviceRoleListView(ObjectListView):
|
||||||
queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
|
queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||||
table = tables.DeviceRoleTable
|
table = tables.DeviceRoleTable
|
||||||
edit_permissions = ['dcim.change_devicerole', 'dcim.delete_devicerole']
|
|
||||||
template_name = 'dcim/devicerole_list.html'
|
template_name = 'dcim/devicerole_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -533,14 +612,15 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'dcim.change_devicerole'
|
permission_required = 'dcim.change_devicerole'
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
form_class = forms.DeviceRoleForm
|
form_class = forms.DeviceRoleForm
|
||||||
obj_list_url = 'dcim:devicerole_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('dcim:devicerole_list')
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_devicerole'
|
permission_required = 'dcim.delete_devicerole'
|
||||||
cls = DeviceRole
|
cls = DeviceRole
|
||||||
default_redirect_url = 'dcim:devicerole_list'
|
default_return_url = 'dcim:devicerole_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -550,7 +630,6 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
class PlatformListView(ObjectListView):
|
class PlatformListView(ObjectListView):
|
||||||
queryset = Platform.objects.annotate(device_count=Count('devices'))
|
queryset = Platform.objects.annotate(device_count=Count('devices'))
|
||||||
table = tables.PlatformTable
|
table = tables.PlatformTable
|
||||||
edit_permissions = ['dcim.change_platform', 'dcim.delete_platform']
|
|
||||||
template_name = 'dcim/platform_list.html'
|
template_name = 'dcim/platform_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -558,14 +637,15 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'dcim.change_platform'
|
permission_required = 'dcim.change_platform'
|
||||||
model = Platform
|
model = Platform
|
||||||
form_class = forms.PlatformForm
|
form_class = forms.PlatformForm
|
||||||
obj_list_url = 'dcim:platform_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('dcim:platform_list')
|
||||||
|
|
||||||
|
|
||||||
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_platform'
|
permission_required = 'dcim.delete_platform'
|
||||||
cls = Platform
|
cls = Platform
|
||||||
default_redirect_url = 'dcim:platform_list'
|
default_return_url = 'dcim:platform_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -573,18 +653,19 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class DeviceListView(ObjectListView):
|
class DeviceListView(ObjectListView):
|
||||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'rack__site',
|
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack',
|
||||||
'primary_ip4', 'primary_ip6')
|
'primary_ip4', 'primary_ip6')
|
||||||
filter = filters.DeviceFilter
|
filter = filters.DeviceFilter
|
||||||
filter_form = forms.DeviceFilterForm
|
filter_form = forms.DeviceFilterForm
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
edit_permissions = ['dcim.change_device', 'dcim.delete_device']
|
|
||||||
template_name = 'dcim/device_list.html'
|
template_name = 'dcim/device_list.html'
|
||||||
|
|
||||||
|
|
||||||
def device(request, pk):
|
def device(request, pk):
|
||||||
|
|
||||||
device = get_object_or_404(Device, pk=pk)
|
device = get_object_or_404(Device.objects.select_related(
|
||||||
|
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
|
||||||
|
), pk=pk)
|
||||||
console_ports = natsorted(
|
console_ports = natsorted(
|
||||||
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
|
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
|
||||||
)
|
)
|
||||||
@@ -597,16 +678,14 @@ def device(request, pk):
|
|||||||
power_outlets = natsorted(
|
power_outlets = natsorted(
|
||||||
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||||
)
|
)
|
||||||
interfaces = Interface.objects.filter(device=device, mgmt_only=False).select_related(
|
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
|
||||||
'connected_as_a__interface_b__device',
|
.filter(device=device, mgmt_only=False)\
|
||||||
'connected_as_b__interface_a__device',
|
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
||||||
'circuit_termination__circuit',
|
'circuit_termination__circuit')
|
||||||
)
|
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
|
||||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True).select_related(
|
.filter(device=device, mgmt_only=True)\
|
||||||
'connected_as_a__interface_b__device',
|
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
||||||
'connected_as_b__interface_a__device',
|
'circuit_termination__circuit')
|
||||||
'circuit_termination__circuit',
|
|
||||||
)
|
|
||||||
device_bays = natsorted(
|
device_bays = natsorted(
|
||||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||||
key=attrgetter('name')
|
key=attrgetter('name')
|
||||||
@@ -659,13 +738,13 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
form_class = forms.DeviceForm
|
form_class = forms.DeviceForm
|
||||||
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
|
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
|
||||||
template_name = 'dcim/device_edit.html'
|
template_name = 'dcim/device_edit.html'
|
||||||
obj_list_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_device'
|
permission_required = 'dcim.delete_device'
|
||||||
model = Device
|
model = Device
|
||||||
redirect_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -673,7 +752,7 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.DeviceImportForm
|
form = forms.DeviceImportForm
|
||||||
table = tables.DeviceImportTable
|
table = tables.DeviceImportTable
|
||||||
template_name = 'dcim/device_import.html'
|
template_name = 'dcim/device_import.html'
|
||||||
obj_list_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -681,7 +760,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.ChildDeviceImportForm
|
form = forms.ChildDeviceImportForm
|
||||||
table = tables.DeviceImportTable
|
table = tables.DeviceImportTable
|
||||||
template_name = 'dcim/device_import_child.html'
|
template_name = 'dcim/device_import_child.html'
|
||||||
obj_list_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
def save_obj(self, obj):
|
def save_obj(self, obj):
|
||||||
# Inherent rack from parent device
|
# Inherent rack from parent device
|
||||||
@@ -696,15 +775,17 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_device'
|
permission_required = 'dcim.change_device'
|
||||||
cls = Device
|
cls = Device
|
||||||
|
filter = filters.DeviceFilter
|
||||||
form = forms.DeviceBulkEditForm
|
form = forms.DeviceBulkEditForm
|
||||||
template_name = 'dcim/device_bulk_edit.html'
|
template_name = 'dcim/device_bulk_edit.html'
|
||||||
default_redirect_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_device'
|
permission_required = 'dcim.delete_device'
|
||||||
cls = Device
|
cls = Device
|
||||||
default_redirect_url = 'dcim:device_list'
|
filter = filters.DeviceFilter
|
||||||
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
def device_inventory(request, pk):
|
def device_inventory(request, pk):
|
||||||
@@ -722,7 +803,8 @@ def device_inventory(request, pk):
|
|||||||
def device_lldp_neighbors(request, pk):
|
def device_lldp_neighbors(request, pk):
|
||||||
|
|
||||||
device = get_object_or_404(Device, pk=pk)
|
device = get_object_or_404(Device, pk=pk)
|
||||||
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b')
|
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
|
||||||
|
.select_related('connected_as_a', 'connected_as_b')
|
||||||
|
|
||||||
return render(request, 'dcim/device_lldp_neighbors.html', {
|
return render(request, 'dcim/device_lldp_neighbors.html', {
|
||||||
'device': device,
|
'device': device,
|
||||||
@@ -769,7 +851,7 @@ def consoleport_connect(request, pk):
|
|||||||
return render(request, 'dcim/consoleport_connect.html', {
|
return render(request, 'dcim/consoleport_connect.html', {
|
||||||
'consoleport': consoleport,
|
'consoleport': consoleport,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -798,17 +880,17 @@ def consoleport_disconnect(request, pk):
|
|||||||
return render(request, 'dcim/consoleport_disconnect.html', {
|
return render(request, 'dcim/consoleport_disconnect.html', {
|
||||||
'consoleport': consoleport,
|
'consoleport': consoleport,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortEditView(PermissionRequiredMixin, ObjectEditView):
|
class ConsolePortEditView(PermissionRequiredMixin, ComponentEditView):
|
||||||
permission_required = 'dcim.change_consoleport'
|
permission_required = 'dcim.change_consoleport'
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
form_class = forms.ConsolePortForm
|
form_class = forms.ConsolePortForm
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ConsolePortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
||||||
permission_required = 'dcim.delete_consoleport'
|
permission_required = 'dcim.delete_consoleport'
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
|
|
||||||
@@ -865,7 +947,7 @@ def consoleserverport_connect(request, pk):
|
|||||||
return render(request, 'dcim/consoleserverport_connect.html', {
|
return render(request, 'dcim/consoleserverport_connect.html', {
|
||||||
'consoleserverport': consoleserverport,
|
'consoleserverport': consoleserverport,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -895,17 +977,17 @@ def consoleserverport_disconnect(request, pk):
|
|||||||
return render(request, 'dcim/consoleserverport_disconnect.html', {
|
return render(request, 'dcim/consoleserverport_disconnect.html', {
|
||||||
'consoleserverport': consoleserverport,
|
'consoleserverport': consoleserverport,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortEditView(PermissionRequiredMixin, ObjectEditView):
|
class ConsoleServerPortEditView(PermissionRequiredMixin, ComponentEditView):
|
||||||
permission_required = 'dcim.change_consoleserverport'
|
permission_required = 'dcim.change_consoleserverport'
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
form_class = forms.ConsoleServerPortForm
|
form_class = forms.ConsoleServerPortForm
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
||||||
permission_required = 'dcim.delete_consoleserverport'
|
permission_required = 'dcim.delete_consoleserverport'
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
|
|
||||||
@@ -955,7 +1037,7 @@ def powerport_connect(request, pk):
|
|||||||
return render(request, 'dcim/powerport_connect.html', {
|
return render(request, 'dcim/powerport_connect.html', {
|
||||||
'powerport': powerport,
|
'powerport': powerport,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -984,17 +1066,17 @@ def powerport_disconnect(request, pk):
|
|||||||
return render(request, 'dcim/powerport_disconnect.html', {
|
return render(request, 'dcim/powerport_disconnect.html', {
|
||||||
'powerport': powerport,
|
'powerport': powerport,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class PowerPortEditView(PermissionRequiredMixin, ObjectEditView):
|
class PowerPortEditView(PermissionRequiredMixin, ComponentEditView):
|
||||||
permission_required = 'dcim.change_powerport'
|
permission_required = 'dcim.change_powerport'
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
form_class = forms.PowerPortForm
|
form_class = forms.PowerPortForm
|
||||||
|
|
||||||
|
|
||||||
class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class PowerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
||||||
permission_required = 'dcim.delete_powerport'
|
permission_required = 'dcim.delete_powerport'
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
|
|
||||||
@@ -1051,7 +1133,7 @@ def poweroutlet_connect(request, pk):
|
|||||||
return render(request, 'dcim/poweroutlet_connect.html', {
|
return render(request, 'dcim/poweroutlet_connect.html', {
|
||||||
'poweroutlet': poweroutlet,
|
'poweroutlet': poweroutlet,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1080,17 +1162,17 @@ def poweroutlet_disconnect(request, pk):
|
|||||||
return render(request, 'dcim/poweroutlet_disconnect.html', {
|
return render(request, 'dcim/poweroutlet_disconnect.html', {
|
||||||
'poweroutlet': poweroutlet,
|
'poweroutlet': poweroutlet,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletEditView(PermissionRequiredMixin, ObjectEditView):
|
class PowerOutletEditView(PermissionRequiredMixin, ComponentEditView):
|
||||||
permission_required = 'dcim.change_poweroutlet'
|
permission_required = 'dcim.change_poweroutlet'
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
form_class = forms.PowerOutletForm
|
form_class = forms.PowerOutletForm
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
||||||
permission_required = 'dcim.delete_poweroutlet'
|
permission_required = 'dcim.delete_poweroutlet'
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
|
|
||||||
@@ -1114,13 +1196,13 @@ class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
|
|||||||
model_form = forms.InterfaceForm
|
model_form = forms.InterfaceForm
|
||||||
|
|
||||||
|
|
||||||
class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
class InterfaceEditView(PermissionRequiredMixin, ComponentEditView):
|
||||||
permission_required = 'dcim.change_interface'
|
permission_required = 'dcim.change_interface'
|
||||||
model = Interface
|
model = Interface
|
||||||
form_class = forms.InterfaceForm
|
form_class = forms.InterfaceForm
|
||||||
|
|
||||||
|
|
||||||
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
||||||
permission_required = 'dcim.delete_interface'
|
permission_required = 'dcim.delete_interface'
|
||||||
model = Interface
|
model = Interface
|
||||||
|
|
||||||
@@ -1152,13 +1234,13 @@ class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
|
|||||||
model_form = forms.DeviceBayForm
|
model_form = forms.DeviceBayForm
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayEditView(PermissionRequiredMixin, ObjectEditView):
|
class DeviceBayEditView(PermissionRequiredMixin, ComponentEditView):
|
||||||
permission_required = 'dcim.change_devicebay'
|
permission_required = 'dcim.change_devicebay'
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
form_class = forms.DeviceBayForm
|
form_class = forms.DeviceBayForm
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class DeviceBayDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
||||||
permission_required = 'dcim.delete_devicebay'
|
permission_required = 'dcim.delete_devicebay'
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
|
|
||||||
@@ -1185,7 +1267,7 @@ def devicebay_populate(request, pk):
|
|||||||
return render(request, 'dcim/devicebay_populate.html', {
|
return render(request, 'dcim/devicebay_populate.html', {
|
||||||
'device_bay': device_bay,
|
'device_bay': device_bay,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1209,7 +1291,7 @@ def devicebay_depopulate(request, pk):
|
|||||||
return render(request, 'dcim/devicebay_depopulate.html', {
|
return render(request, 'dcim/devicebay_depopulate.html', {
|
||||||
'device_bay': device_bay,
|
'device_bay': device_bay,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1238,7 +1320,7 @@ class DeviceBulkAddComponentView(View):
|
|||||||
|
|
||||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||||
if request.POST.get('_all'):
|
if request.POST.get('_all'):
|
||||||
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
pk_list = [obj.pk for obj in filters.DeviceFilter(request.GET, Device.objects.all())]
|
||||||
else:
|
else:
|
||||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||||
|
|
||||||
@@ -1284,7 +1366,7 @@ class DeviceBulkAddComponentView(View):
|
|||||||
'form': form,
|
'form': form,
|
||||||
'component_name': self.model._meta.verbose_name_plural,
|
'component_name': self.model._meta.verbose_name_plural,
|
||||||
'selected_devices': selected_devices,
|
'selected_devices': selected_devices,
|
||||||
'cancel_url': reverse('dcim:device_list'),
|
'return_url': reverse('dcim:device_list'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1357,7 +1439,7 @@ def interfaceconnection_add(request, pk):
|
|||||||
else:
|
else:
|
||||||
form = forms.InterfaceConnectionForm(device, initial={
|
form = forms.InterfaceConnectionForm(device, initial={
|
||||||
'interface_a': request.GET.get('interface_a', None),
|
'interface_a': request.GET.get('interface_a', None),
|
||||||
'site_b': request.GET.get('site_b', device.rack.site),
|
'site_b': request.GET.get('site_b', device.site),
|
||||||
'rack_b': request.GET.get('rack_b', None),
|
'rack_b': request.GET.get('rack_b', None),
|
||||||
'device_b': request.GET.get('device_b', None),
|
'device_b': request.GET.get('device_b', None),
|
||||||
'interface_b': request.GET.get('interface_b', None),
|
'interface_b': request.GET.get('interface_b', None),
|
||||||
@@ -1366,7 +1448,7 @@ def interfaceconnection_add(request, pk):
|
|||||||
return render(request, 'dcim/interfaceconnection_edit.html', {
|
return render(request, 'dcim/interfaceconnection_edit.html', {
|
||||||
'device': device,
|
'device': device,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1398,15 +1480,15 @@ def interfaceconnection_delete(request, pk):
|
|||||||
|
|
||||||
# Determine where to direct user upon cancellation
|
# Determine where to direct user upon cancellation
|
||||||
if device_id:
|
if device_id:
|
||||||
cancel_url = reverse('dcim:device', kwargs={'pk': device_id})
|
return_url = reverse('dcim:device', kwargs={'pk': device_id})
|
||||||
else:
|
else:
|
||||||
cancel_url = reverse('dcim:device_list')
|
return_url = reverse('dcim:device_list')
|
||||||
|
|
||||||
return render(request, 'dcim/interfaceconnection_delete.html', {
|
return render(request, 'dcim/interfaceconnection_delete.html', {
|
||||||
'interfaceconnection': interfaceconnection,
|
'interfaceconnection': interfaceconnection,
|
||||||
'device_id': device_id,
|
'device_id': device_id,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': cancel_url,
|
'return_url': return_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1485,7 +1567,7 @@ def ipaddress_assign(request, pk):
|
|||||||
return render(request, 'dcim/ipaddress_assign.html', {
|
return render(request, 'dcim/ipaddress_assign.html', {
|
||||||
'device': device,
|
'device': device,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
'return_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1493,18 +1575,17 @@ def ipaddress_assign(request, pk):
|
|||||||
# Modules
|
# Modules
|
||||||
#
|
#
|
||||||
|
|
||||||
class ModuleEditView(PermissionRequiredMixin, ObjectEditView):
|
class ModuleEditView(PermissionRequiredMixin, ComponentEditView):
|
||||||
permission_required = 'dcim.change_module'
|
permission_required = 'dcim.change_module'
|
||||||
model = Module
|
model = Module
|
||||||
form_class = forms.ModuleForm
|
form_class = forms.ModuleForm
|
||||||
|
|
||||||
def alter_obj(self, obj, args, kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
if 'device' in kwargs:
|
if 'device' in url_kwargs:
|
||||||
device = get_object_or_404(Device, pk=kwargs['device'])
|
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||||
obj.device = device
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class ModuleDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ModuleDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
||||||
permission_required = 'dcim.delete_module'
|
permission_required = 'dcim.delete_module'
|
||||||
model = Module
|
model = Module
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class FlatJSONRenderer(renderers.BaseRenderer):
|
|||||||
def render(self, data, media_type=None, renderer_context=None):
|
def render(self, data, media_type=None, renderer_context=None):
|
||||||
|
|
||||||
def flatten(entry):
|
def flatten(entry):
|
||||||
for key, val in entry.iteritems():
|
for key, val in entry.items():
|
||||||
if isinstance(val, dict):
|
if isinstance(val, dict):
|
||||||
for child_key, child_val in flatten(val):
|
for child_key, child_val in flatten(val):
|
||||||
yield "{}_{}".format(key, child_key), child_val
|
yield "{}_{}".format(key, child_key), child_val
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from django.core.validators import ValidationError
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ class CustomFieldModel(object):
|
|||||||
return OrderedDict([(field, None) for field in fields])
|
return OrderedDict([(field, None) for field in fields])
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class CustomField(models.Model):
|
class CustomField(models.Model):
|
||||||
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
|
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
|
||||||
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
|
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
|
||||||
@@ -114,7 +116,7 @@ class CustomField(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['weight', 'name']
|
ordering = ['weight', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.label or self.name.replace('_', ' ').capitalize()
|
return self.label or self.name.replace('_', ' ').capitalize()
|
||||||
|
|
||||||
def serialize_value(self, value):
|
def serialize_value(self, value):
|
||||||
@@ -153,6 +155,7 @@ class CustomField(models.Model):
|
|||||||
return serialized_value
|
return serialized_value
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class CustomFieldValue(models.Model):
|
class CustomFieldValue(models.Model):
|
||||||
field = models.ForeignKey('CustomField', related_name='values')
|
field = models.ForeignKey('CustomField', related_name='values')
|
||||||
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
|
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
|
||||||
@@ -164,7 +167,7 @@ class CustomFieldValue(models.Model):
|
|||||||
ordering = ['obj_type', 'obj_id']
|
ordering = ['obj_type', 'obj_id']
|
||||||
unique_together = ['field', 'obj_type', 'obj_id']
|
unique_together = ['field', 'obj_type', 'obj_id']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return u'{} {}'.format(self.obj, self.field)
|
return u'{} {}'.format(self.obj, self.field)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -183,6 +186,7 @@ class CustomFieldValue(models.Model):
|
|||||||
super(CustomFieldValue, self).save(*args, **kwargs)
|
super(CustomFieldValue, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class CustomFieldChoice(models.Model):
|
class CustomFieldChoice(models.Model):
|
||||||
field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
|
field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
|
||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE)
|
||||||
@@ -193,7 +197,7 @@ class CustomFieldChoice(models.Model):
|
|||||||
ordering = ['field', 'weight', 'value']
|
ordering = ['field', 'weight', 'value']
|
||||||
unique_together = ['field', 'value']
|
unique_together = ['field', 'value']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@@ -207,6 +211,7 @@ class CustomFieldChoice(models.Model):
|
|||||||
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
|
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Graph(models.Model):
|
class Graph(models.Model):
|
||||||
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
|
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
|
||||||
weight = models.PositiveSmallIntegerField(default=1000)
|
weight = models.PositiveSmallIntegerField(default=1000)
|
||||||
@@ -217,7 +222,7 @@ class Graph(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['type', 'weight', 'name']
|
ordering = ['type', 'weight', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def embed_url(self, obj):
|
def embed_url(self, obj):
|
||||||
@@ -231,6 +236,7 @@ class Graph(models.Model):
|
|||||||
return template.render(Context({'obj': obj}))
|
return template.render(Context({'obj': obj}))
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class ExportTemplate(models.Model):
|
class ExportTemplate(models.Model):
|
||||||
content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
|
content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
@@ -245,7 +251,7 @@ class ExportTemplate(models.Model):
|
|||||||
['content_type', 'name']
|
['content_type', 'name']
|
||||||
]
|
]
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return u'{}: {}'.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):
|
||||||
@@ -264,6 +270,7 @@ class ExportTemplate(models.Model):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class TopologyMap(models.Model):
|
class TopologyMap(models.Model):
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(max_length=50, unique=True)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
@@ -278,7 +285,7 @@ class TopologyMap(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -328,6 +335,7 @@ class UserActionManager(models.Manager):
|
|||||||
self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
|
self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class UserAction(models.Model):
|
class UserAction(models.Model):
|
||||||
"""
|
"""
|
||||||
A record of an action (add, edit, or delete) performed on an object by a User.
|
A record of an action (add, edit, or delete) performed on an object by a User.
|
||||||
@@ -344,7 +352,7 @@ class UserAction(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-time']
|
ordering = ['-time']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
if self.message:
|
if self.message:
|
||||||
return u'{} {}'.format(self.user, self.message)
|
return u'{} {}'.format(self.user, self.message)
|
||||||
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
# This script will generate a random 50-character string suitable for use as a SECRET_KEY.
|
# This script will generate a random 50-character string suitable for use as a SECRET_KEY.
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
|
||||||
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
|
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
|
||||||
random.seed = (os.urandom(2048))
|
random.seed = (os.urandom(2048))
|
||||||
print ''.join(random.choice(charset) for c in range(50))
|
print(''.join(random.choice(charset) for c in range(50)))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
from .formfields import IPFormField
|
from .formfields import IPFormField
|
||||||
from .lookups import (
|
from .lookups import (
|
||||||
EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals,
|
EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals,
|
||||||
NetHost, Regex, StartsWith,
|
NetHost, NetMaskLength, Regex, StartsWith,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ IPNetworkField.register_lookup(NetContainedOrEqual)
|
|||||||
IPNetworkField.register_lookup(NetContains)
|
IPNetworkField.register_lookup(NetContains)
|
||||||
IPNetworkField.register_lookup(NetContainsOrEquals)
|
IPNetworkField.register_lookup(NetContainsOrEquals)
|
||||||
IPNetworkField.register_lookup(NetHost)
|
IPNetworkField.register_lookup(NetHost)
|
||||||
|
IPNetworkField.register_lookup(NetMaskLength)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressField(BaseIPField):
|
class IPAddressField(BaseIPField):
|
||||||
@@ -90,3 +91,4 @@ IPAddressField.register_lookup(NetContainedOrEqual)
|
|||||||
IPAddressField.register_lookup(NetContains)
|
IPAddressField.register_lookup(NetContains)
|
||||||
IPAddressField.register_lookup(NetContainsOrEquals)
|
IPAddressField.register_lookup(NetContainsOrEquals)
|
||||||
IPAddressField.register_lookup(NetHost)
|
IPAddressField.register_lookup(NetHost)
|
||||||
|
IPAddressField.register_lookup(NetMaskLength)
|
||||||
|
|||||||
@@ -13,15 +13,10 @@ from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLAN
|
|||||||
|
|
||||||
|
|
||||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
name = django_filters.CharFilter(
|
|
||||||
name='name',
|
|
||||||
lookup_type='icontains',
|
|
||||||
label='Name',
|
|
||||||
)
|
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
@@ -34,7 +29,9 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(rd__icontains=value) |
|
Q(rd__icontains=value) |
|
||||||
@@ -43,7 +40,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['rd']
|
fields = ['name', 'rd']
|
||||||
|
|
||||||
|
|
||||||
class RIRFilter(django_filters.FilterSet):
|
class RIRFilter(django_filters.FilterSet):
|
||||||
@@ -54,8 +51,8 @@ class RIRFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
@@ -74,7 +71,9 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
model = Aggregate
|
model = Aggregate
|
||||||
fields = ['family', 'date_added']
|
fields = ['family', 'date_added']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
try:
|
try:
|
||||||
prefix = str(IPNetwork(value.strip()).cidr)
|
prefix = str(IPNetwork(value.strip()).cidr)
|
||||||
@@ -85,14 +84,18 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
parent = django_filters.MethodFilter(
|
parent = django_filters.CharFilter(
|
||||||
action='search_by_parent',
|
method='search_by_parent',
|
||||||
label='Parent prefix',
|
label='Parent prefix',
|
||||||
)
|
)
|
||||||
|
mask_length = django_filters.NumberFilter(
|
||||||
|
method='filter_mask_length',
|
||||||
|
label='Mask length',
|
||||||
|
)
|
||||||
vrf_id = NullableModelMultipleChoiceFilter(
|
vrf_id = NullableModelMultipleChoiceFilter(
|
||||||
name='vrf_id',
|
name='vrf_id',
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
@@ -151,7 +154,9 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['family', 'status']
|
fields = ['family', 'status']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
try:
|
try:
|
||||||
prefix = str(IPNetwork(value.strip()).cidr)
|
prefix = str(IPNetwork(value.strip()).cidr)
|
||||||
@@ -160,7 +165,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
def search_by_parent(self, queryset, value):
|
def search_by_parent(self, queryset, name, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
if not value:
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
@@ -170,34 +175,25 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
def _tenant(self, queryset, value):
|
def filter_mask_length(self, queryset, name, value):
|
||||||
if str(value) == '':
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(prefix__net_mask_length=value)
|
||||||
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(CustomFieldFilterSet, django_filters.FilterSet):
|
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
parent = django_filters.MethodFilter(
|
parent = django_filters.CharFilter(
|
||||||
action='search_by_parent',
|
method='search_by_parent',
|
||||||
label='Parent prefix',
|
label='Parent prefix',
|
||||||
)
|
)
|
||||||
|
mask_length = django_filters.NumberFilter(
|
||||||
|
method='filter_mask_length',
|
||||||
|
label='Mask length',
|
||||||
|
)
|
||||||
vrf_id = NullableModelMultipleChoiceFilter(
|
vrf_id = NullableModelMultipleChoiceFilter(
|
||||||
name='vrf_id',
|
name='vrf_id',
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
@@ -239,9 +235,11 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['q', 'family', 'status']
|
fields = ['family', 'status']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
try:
|
try:
|
||||||
ipaddress = str(IPNetwork(value.strip()))
|
ipaddress = str(IPNetwork(value.strip()))
|
||||||
@@ -250,25 +248,30 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
def search_by_parent(self, queryset, value):
|
def search_by_parent(self, queryset, name, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
if not value:
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
try:
|
try:
|
||||||
query = str(IPNetwork(value).cidr)
|
query = str(IPNetwork(value.strip()).cidr)
|
||||||
return queryset.filter(address__net_contained_or_equal=query)
|
return queryset.filter(address__net_contained_or_equal=query)
|
||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
|
def filter_mask_length(self, queryset, name, value):
|
||||||
|
if not value:
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(address__net_mask_length=value)
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupFilter(django_filters.FilterSet):
|
class VLANGroupFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = NullableModelMultipleChoiceFilter(
|
||||||
name='site__slug',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@@ -276,20 +279,21 @@ class VLANGroupFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = NullableModelMultipleChoiceFilter(
|
||||||
name='site__slug',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@@ -305,15 +309,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Group',
|
label='Group',
|
||||||
)
|
)
|
||||||
name = django_filters.CharFilter(
|
|
||||||
name='name',
|
|
||||||
lookup_type='icontains',
|
|
||||||
label='Name',
|
|
||||||
)
|
|
||||||
vid = django_filters.NumberFilter(
|
|
||||||
name='vid',
|
|
||||||
label='VLAN number (1-4095)',
|
|
||||||
)
|
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
@@ -339,12 +334,14 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['status']
|
fields = ['name', 'vid', 'status']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||||
try:
|
try:
|
||||||
qs_filter |= Q(vid=int(value))
|
qs_filter |= Q(vid=int(value.strip()))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ IP_FAMILY_CHOICES = [
|
|||||||
(6, 'IPv6'),
|
(6, 'IPv6'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
PREFIX_MASK_LENGTH_CHOICES = [
|
||||||
|
('', '---------'),
|
||||||
|
] + [(i, i) for i in range(1, 128)]
|
||||||
|
|
||||||
|
IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VRFs
|
# VRFs
|
||||||
@@ -63,6 +69,7 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
|
|
||||||
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = VRF
|
model = VRF
|
||||||
|
q = forms.CharField(required=False, label='Search')
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
|
||||||
null_option=(0, None))
|
null_option=(0, None))
|
||||||
|
|
||||||
@@ -128,9 +135,13 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
|
|
||||||
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
|
q = forms.CharField(required=False, label='Search')
|
||||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||||
rir = FilterChoiceField(queryset=RIR.objects.annotate(filter_count=Count('aggregates')), to_field_name='slug',
|
rir = FilterChoiceField(
|
||||||
label='RIR')
|
queryset=RIR.objects.annotate(filter_count=Count('aggregates')),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='RIR'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -151,7 +162,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class PrefixForm(BootstrapMixin, CustomFieldForm):
|
class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
||||||
widget=forms.Select(attrs={'filter-for': 'vlan'}))
|
widget=forms.Select(attrs={'filter-for': 'vlan', 'nullable': 'true'}))
|
||||||
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
||||||
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
|
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
|
||||||
display_field='display_name'))
|
display_field='display_name'))
|
||||||
@@ -171,7 +182,7 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
|
|||||||
elif self.initial.get('site'):
|
elif self.initial.get('site'):
|
||||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
||||||
else:
|
else:
|
||||||
self.fields['vlan'].choices = []
|
self.fields['vlan'].queryset = VLAN.objects.filter(site=None)
|
||||||
|
|
||||||
|
|
||||||
class PrefixFromCSVForm(forms.ModelForm):
|
class PrefixFromCSVForm(forms.ModelForm):
|
||||||
@@ -215,6 +226,8 @@ class PrefixFromCSVForm(forms.ModelForm):
|
|||||||
elif vlan_vid and site:
|
elif vlan_vid and site:
|
||||||
try:
|
try:
|
||||||
self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid)
|
self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid)
|
||||||
|
except VLAN.DoesNotExist:
|
||||||
|
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
|
||||||
except VLAN.MultipleObjectsReturned:
|
except VLAN.MultipleObjectsReturned:
|
||||||
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
|
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
|
||||||
elif vlan_vid:
|
elif vlan_vid:
|
||||||
@@ -254,19 +267,34 @@ def prefix_status_choices():
|
|||||||
|
|
||||||
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
q = forms.CharField(required=False, label='Search')
|
||||||
'placeholder': 'Network',
|
parent = forms.CharField(required=False, label='Parent prefix', widget=forms.TextInput(attrs={
|
||||||
|
'placeholder': 'Prefix',
|
||||||
}))
|
}))
|
||||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
|
||||||
vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('prefixes')), to_field_name='rd',
|
mask_length = forms.ChoiceField(required=False, choices=PREFIX_MASK_LENGTH_CHOICES, label='Mask length')
|
||||||
label='VRF', null_option=(0, 'Global'))
|
vrf = FilterChoiceField(
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
|
||||||
null_option=(0, 'None'))
|
to_field_name='rd',
|
||||||
|
label='VRF',
|
||||||
|
null_option=(0, 'Global')
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
|
status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
site = FilterChoiceField(
|
||||||
null_option=(0, 'None'))
|
queryset=Site.objects.annotate(filter_count=Count('prefixes')),
|
||||||
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None'))
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
role = FilterChoiceField(
|
||||||
|
queryset=Role.objects.annotate(filter_count=Count('prefixes')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
||||||
|
|
||||||
|
|
||||||
@@ -302,10 +330,10 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
|||||||
nat_inside = self.instance.nat_inside
|
nat_inside = self.instance.nat_inside
|
||||||
# If the IP is assigned to an interface, populate site/device fields accordingly
|
# If the IP is assigned to an interface, populate site/device fields accordingly
|
||||||
if self.instance.nat_inside.interface:
|
if self.instance.nat_inside.interface:
|
||||||
self.initial['nat_site'] = self.instance.nat_inside.interface.device.rack.site.pk
|
self.initial['nat_site'] = self.instance.nat_inside.interface.device.site.pk
|
||||||
self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
|
self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
|
||||||
self.fields['nat_device'].queryset = Device.objects.filter(
|
self.fields['nat_device'].queryset = Device.objects.filter(
|
||||||
rack__site=nat_inside.interface.device.rack.site)
|
rack__site=nat_inside.interface.device.site)
|
||||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
||||||
interface__device=nat_inside.interface.device)
|
interface__device=nat_inside.interface.device)
|
||||||
else:
|
else:
|
||||||
@@ -334,25 +362,61 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
|||||||
|
|
||||||
class IPAddressBulkAddForm(BootstrapMixin, forms.Form):
|
class IPAddressBulkAddForm(BootstrapMixin, forms.Form):
|
||||||
address = ExpandableIPAddressField()
|
address = ExpandableIPAddressField()
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
|
||||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
status = forms.ChoiceField(choices=IPADDRESS_STATUS_CHOICES)
|
status = forms.ChoiceField(choices=IPADDRESS_STATUS_CHOICES)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
site = forms.ModelChoiceField(
|
||||||
widget=forms.Select(attrs={'filter-for': 'rack'}))
|
queryset=Site.objects.all(),
|
||||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
label='Site',
|
||||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', display_field='display_name', attrs={'filter-for': 'device'}))
|
required=False,
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
widget=forms.Select(
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', display_field='display_name', attrs={'filter-for': 'interface'}))
|
attrs={'filter-for': 'rack'}
|
||||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
)
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
)
|
||||||
|
rack = forms.ModelChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
label='Rack',
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
|
display_field='display_name',
|
||||||
|
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device',
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||||
|
display_field='display_name',
|
||||||
|
attrs={'filter-for': 'interface'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
livesearch = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Device',
|
||||||
|
widget=Livesearch(
|
||||||
|
query_key='q',
|
||||||
|
query_url='dcim-api:device_list',
|
||||||
|
field_to_update='device'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
interface = forms.ModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
label='Interface',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/devices/{{device}}/interfaces/'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set_as_primary = forms.BooleanField(
|
||||||
|
label='Set as primary IP for device',
|
||||||
|
required=False
|
||||||
)
|
)
|
||||||
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
|
|
||||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/'))
|
|
||||||
set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
@@ -442,14 +506,23 @@ def ipaddress_status_choices():
|
|||||||
|
|
||||||
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
q = forms.CharField(required=False, label='Search')
|
||||||
|
parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={
|
||||||
'placeholder': 'Prefix',
|
'placeholder': 'Prefix',
|
||||||
}))
|
}))
|
||||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
|
||||||
vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='rd',
|
mask_length = forms.ChoiceField(required=False, choices=IPADDRESS_MASK_LENGTH_CHOICES, label='Mask length')
|
||||||
label='VRF', null_option=(0, 'Global'))
|
vrf = FilterChoiceField(
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
|
||||||
to_field_name='slug', null_option=(0, 'None'))
|
to_field_name='rd',
|
||||||
|
label='VRF',
|
||||||
|
null_option=(0, 'Global')
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
|
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
|
||||||
|
|
||||||
|
|
||||||
@@ -466,7 +539,11 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug')
|
site = FilterChoiceField(
|
||||||
|
queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'Global')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -482,7 +559,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which this VLAN exists",
|
'site': "Leave blank if this VLAN spans multiple sites",
|
||||||
'group': "VLAN group (optional)",
|
'group': "VLAN group (optional)",
|
||||||
'vid': "Configured VLAN ID",
|
'vid': "Configured VLAN ID",
|
||||||
'name': "Configured VLAN name",
|
'name': "Configured VLAN name",
|
||||||
@@ -490,7 +567,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
'role': "The primary function of this VLAN",
|
'role': "The primary function of this VLAN",
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'site': forms.Select(attrs={'filter-for': 'group'}),
|
'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -503,11 +580,11 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
elif self.initial.get('site'):
|
elif self.initial.get('site'):
|
||||||
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
||||||
else:
|
else:
|
||||||
self.fields['group'].choices = []
|
self.fields['group'].queryset = VLANGroup.objects.filter(site=None)
|
||||||
|
|
||||||
|
|
||||||
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(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
error_messages={'invalid_choice': 'Site not found.'})
|
||||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'VLAN group not found.'})
|
error_messages={'invalid_choice': 'VLAN group not found.'})
|
||||||
@@ -556,14 +633,28 @@ def vlan_status_choices():
|
|||||||
|
|
||||||
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug')
|
q = forms.CharField(required=False, label='Search')
|
||||||
group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
|
site = FilterChoiceField(
|
||||||
null_option=(0, 'None'))
|
queryset=Site.objects.annotate(filter_count=Count('vlans')),
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None'))
|
null_option=(0, 'Global')
|
||||||
|
)
|
||||||
|
group_id = FilterChoiceField(
|
||||||
|
queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
|
||||||
|
label='VLAN group',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
||||||
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
role = FilterChoiceField(
|
||||||
null_option=(0, 'None'))
|
queryset=Role.objects.annotate(filter_count=Count('vlans')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.db.models import Lookup
|
from django.db.models import Lookup, Transform, IntegerField
|
||||||
from django.db.models.lookups import BuiltinLookup
|
from django.db.models.lookups import BuiltinLookup
|
||||||
|
|
||||||
|
|
||||||
@@ -87,3 +87,12 @@ class NetHost(Lookup):
|
|||||||
rhs_params[0] = rhs_params[0].split('/')[0]
|
rhs_params[0] = rhs_params[0].split('/')[0]
|
||||||
params = lhs_params + rhs_params
|
params = lhs_params + rhs_params
|
||||||
return 'HOST(%s) = %s' % (lhs, rhs), params
|
return 'HOST(%s) = %s' % (lhs, rhs), params
|
||||||
|
|
||||||
|
|
||||||
|
class NetMaskLength(Transform):
|
||||||
|
lookup_name = 'net_mask_length'
|
||||||
|
function = 'MASKLEN'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_field(self):
|
||||||
|
return IntegerField()
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-01-23 19:10
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0013_prefix_add_is_pool'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='status',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(1, b'Active'), (2, b'Reserved'), (3, b'Deprecated'), (5, b'DHCP')], default=1, verbose_name=b'Status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
26
netbox/ipam/migrations/0015_global_vlans.py
Normal file
26
netbox/ipam/migrations/0015_global_vlans.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-21 18:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0014_ipaddress_status_add_deprecated'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='dcim.Site'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlangroup',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_groups', to='dcim.Site'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -7,6 +7,7 @@ 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 django.db.models.expressions import RawSQL
|
from django.db.models.expressions import RawSQL
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
from extras.models import CustomFieldModel, CustomFieldValue
|
||||||
@@ -36,10 +37,12 @@ PREFIX_STATUS_CHOICES = (
|
|||||||
|
|
||||||
IPADDRESS_STATUS_ACTIVE = 1
|
IPADDRESS_STATUS_ACTIVE = 1
|
||||||
IPADDRESS_STATUS_RESERVED = 2
|
IPADDRESS_STATUS_RESERVED = 2
|
||||||
|
IPADDRESS_STATUS_DEPRECATED = 3
|
||||||
IPADDRESS_STATUS_DHCP = 5
|
IPADDRESS_STATUS_DHCP = 5
|
||||||
IPADDRESS_STATUS_CHOICES = (
|
IPADDRESS_STATUS_CHOICES = (
|
||||||
(IPADDRESS_STATUS_ACTIVE, 'Active'),
|
(IPADDRESS_STATUS_ACTIVE, 'Active'),
|
||||||
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
|
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
|
||||||
|
(IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
|
||||||
(IPADDRESS_STATUS_DHCP, 'DHCP')
|
(IPADDRESS_STATUS_DHCP, 'DHCP')
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,6 +73,7 @@ IP_PROTOCOL_CHOICES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class VRF(CreatedUpdatedModel, CustomFieldModel):
|
class VRF(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
|
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
|
||||||
@@ -89,7 +93,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
verbose_name = 'VRF'
|
verbose_name = 'VRF'
|
||||||
verbose_name_plural = 'VRFs'
|
verbose_name_plural = 'VRFs'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -105,6 +109,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class RIR(models.Model):
|
class RIR(models.Model):
|
||||||
"""
|
"""
|
||||||
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
|
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
|
||||||
@@ -120,13 +125,14 @@ class RIR(models.Model):
|
|||||||
verbose_name = 'RIR'
|
verbose_name = 'RIR'
|
||||||
verbose_name_plural = 'RIRs'
|
verbose_name_plural = 'RIRs'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
|
return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
|
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
|
||||||
@@ -142,7 +148,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['family', 'prefix']
|
ordering = ['family', 'prefix']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return str(self.prefix)
|
return str(self.prefix)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -204,6 +210,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return int(children_size / self.prefix.size * 100)
|
return int(children_size / self.prefix.size * 100)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Role(models.Model):
|
class Role(models.Model):
|
||||||
"""
|
"""
|
||||||
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
||||||
@@ -216,7 +223,7 @@ class Role(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['weight', 'name']
|
ordering = ['weight', 'name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -263,6 +270,7 @@ class PrefixQuerySet(NullsFirstQuerySet):
|
|||||||
return filter(lambda p: p.depth <= limit, queryset)
|
return filter(lambda p: p.depth <= limit, queryset)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
|
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
|
||||||
@@ -292,16 +300,20 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
ordering = ['vrf', 'family', 'prefix']
|
ordering = ['vrf', 'family', 'prefix']
|
||||||
verbose_name_plural = 'prefixes'
|
verbose_name_plural = 'prefixes'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return str(self.prefix)
|
return str(self.prefix)
|
||||||
|
|
||||||
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 get_duplicates(self):
|
||||||
|
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Disallow host masks
|
|
||||||
if self.prefix:
|
if self.prefix:
|
||||||
|
|
||||||
|
# Disallow host masks
|
||||||
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
|
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'prefix': "Cannot create host addresses (/32) as prefixes. Create an IPv4 address instead."
|
'prefix': "Cannot create host addresses (/32) as prefixes. Create an IPv4 address instead."
|
||||||
@@ -311,6 +323,17 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
'prefix': "Cannot create host addresses (/128) as prefixes. Create an IPv6 address instead."
|
'prefix': "Cannot create host addresses (/128) as prefixes. Create an IPv6 address instead."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Enforce unique IP space (if applicable)
|
||||||
|
if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
|
||||||
|
duplicate_prefixes = self.get_duplicates()
|
||||||
|
if duplicate_prefixes:
|
||||||
|
raise ValidationError({
|
||||||
|
'prefix': "Duplicate prefix found in {}: {}".format(
|
||||||
|
"VRF {}".format(self.vrf) if self.vrf else "global table",
|
||||||
|
duplicate_prefixes.first(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
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
|
||||||
@@ -362,6 +385,7 @@ class IPAddressManager(models.Manager):
|
|||||||
return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
|
return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
@@ -394,29 +418,29 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
verbose_name = 'IP address'
|
verbose_name = 'IP address'
|
||||||
verbose_name_plural = 'IP addresses'
|
verbose_name_plural = 'IP addresses'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return str(self.address)
|
return str(self.address)
|
||||||
|
|
||||||
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 get_duplicates(self):
|
||||||
|
return IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip)).exclude(pk=self.pk)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Enforce unique IP space if applicable
|
if self.address:
|
||||||
if self.vrf and self.vrf.enforce_unique:
|
|
||||||
duplicate_ips = IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip))\
|
# Enforce unique IP space (if applicable)
|
||||||
.exclude(pk=self.pk)
|
if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
|
||||||
if duplicate_ips:
|
duplicate_ips = self.get_duplicates()
|
||||||
raise ValidationError({
|
if duplicate_ips:
|
||||||
'address': "Duplicate IP address found in VRF {}: {}".format(self.vrf, duplicate_ips.first())
|
raise ValidationError({
|
||||||
})
|
'address': "Duplicate IP address found in {}: {}".format(
|
||||||
elif not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE:
|
"VRF {}".format(self.vrf) if self.vrf else "global table",
|
||||||
duplicate_ips = IPAddress.objects.filter(vrf=None, address__net_host=str(self.address.ip))\
|
duplicate_ips.first(),
|
||||||
.exclude(pk=self.pk)
|
)
|
||||||
if duplicate_ips:
|
})
|
||||||
raise ValidationError({
|
|
||||||
'address': "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:
|
||||||
@@ -454,13 +478,14 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return STATUS_CHOICE_CLASSES[self.status]
|
return STATUS_CHOICE_CLASSES[self.status]
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class VLANGroup(models.Model):
|
class VLANGroup(models.Model):
|
||||||
"""
|
"""
|
||||||
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
site = models.ForeignKey('dcim.Site', related_name='vlan_groups')
|
site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['site', 'name']
|
ordering = ['site', 'name']
|
||||||
@@ -471,13 +496,16 @@ class VLANGroup(models.Model):
|
|||||||
verbose_name = 'VLAN group'
|
verbose_name = 'VLAN group'
|
||||||
verbose_name_plural = 'VLAN groups'
|
verbose_name_plural = 'VLAN groups'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
|
if self.site is None:
|
||||||
|
return self.name
|
||||||
return u'{} - {}'.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('ipam:vlan_list'), self.pk)
|
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
@@ -487,7 +515,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
|
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.
|
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, blank=True, null=True)
|
||||||
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, 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),
|
||||||
@@ -509,7 +537,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
verbose_name = 'VLAN'
|
verbose_name = 'VLAN'
|
||||||
verbose_name_plural = 'VLANs'
|
verbose_name_plural = 'VLANs'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.display_name
|
return self.display_name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -525,7 +553,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return csv_format([
|
return csv_format([
|
||||||
self.site.name,
|
self.site.name if self.site else None,
|
||||||
self.group.name if self.group else None,
|
self.group.name if self.group else None,
|
||||||
self.vid,
|
self.vid,
|
||||||
self.name,
|
self.name,
|
||||||
@@ -543,6 +571,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return STATUS_CHOICE_CLASSES[self.status]
|
return STATUS_CHOICE_CLASSES[self.status]
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Service(CreatedUpdatedModel):
|
class Service(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied
|
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied
|
||||||
@@ -561,8 +590,5 @@ class Service(CreatedUpdatedModel):
|
|||||||
ordering = ['device', 'protocol', 'port']
|
ordering = ['device', 'protocol', 'port']
|
||||||
unique_together = ['device', 'protocol', 'port']
|
unique_together = ['device', 'protocol', 'port']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
||||||
|
|
||||||
def get_parent_url(self):
|
|
||||||
return self.device.get_absolute_url()
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class VRFTable(BaseTable):
|
|||||||
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')
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
description = tables.Column(orderable=False, verbose_name='Description')
|
description = tables.Column(verbose_name='Description')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VRF
|
model = VRF
|
||||||
@@ -182,7 +182,7 @@ class AggregateTable(BaseTable):
|
|||||||
child_count = tables.Column(verbose_name='Prefixes')
|
child_count = tables.Column(verbose_name='Prefixes')
|
||||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
||||||
description = tables.Column(orderable=False, verbose_name='Description')
|
description = tables.Column(verbose_name='Description')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
@@ -219,7 +219,7 @@ class PrefixTable(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')
|
||||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
||||||
role = tables.TemplateColumn(PREFIX_ROLE_LINK, verbose_name='Role')
|
role = tables.TemplateColumn(PREFIX_ROLE_LINK, verbose_name='Role')
|
||||||
description = tables.Column(orderable=False, verbose_name='Description')
|
description = tables.Column(verbose_name='Description')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
@@ -234,11 +234,12 @@ class PrefixBriefTable(BaseTable):
|
|||||||
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
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')
|
||||||
|
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
||||||
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', 'vrf', 'status', 'site', 'role')
|
fields = ('prefix', 'vrf', 'status', 'site', 'vlan', 'role')
|
||||||
orderable = False
|
orderable = False
|
||||||
|
|
||||||
|
|
||||||
@@ -255,7 +256,7 @@ class IPAddressTable(BaseTable):
|
|||||||
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')
|
||||||
description = tables.Column(orderable=False, verbose_name='Description')
|
description = tables.Column(verbose_name='Description')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
@@ -310,7 +311,8 @@ class VLANTable(BaseTable):
|
|||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
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.TemplateColumn(VLAN_ROLE_LINK, verbose_name='Role')
|
role = tables.TemplateColumn(VLAN_ROLE_LINK, verbose_name='Role')
|
||||||
|
description = tables.Column(verbose_name='Description')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role')
|
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||||
|
|||||||
0
netbox/ipam/tests/__init__.py
Normal file
0
netbox/ipam/tests/__init__.py
Normal file
60
netbox/ipam/tests/test_models.py
Normal file
60
netbox/ipam/tests/test_models.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import netaddr
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
from ipam.models import IPAddress, Prefix, VRF
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrefix(TestCase):
|
||||||
|
|
||||||
|
@override_settings(ENFORCE_GLOBAL_UNIQUE=False)
|
||||||
|
def test_duplicate_global(self):
|
||||||
|
Prefix.objects.create(prefix=netaddr.IPNetwork('192.0.2.0/24'))
|
||||||
|
duplicate_prefix = Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24'))
|
||||||
|
self.assertIsNone(duplicate_prefix.clean())
|
||||||
|
|
||||||
|
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
|
||||||
|
def test_duplicate_global_unique(self):
|
||||||
|
Prefix.objects.create(prefix=netaddr.IPNetwork('192.0.2.0/24'))
|
||||||
|
duplicate_prefix = Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24'))
|
||||||
|
self.assertRaises(ValidationError, duplicate_prefix.clean)
|
||||||
|
|
||||||
|
def test_duplicate_vrf(self):
|
||||||
|
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=False)
|
||||||
|
Prefix.objects.create(vrf=vrf, prefix=netaddr.IPNetwork('192.0.2.0/24'))
|
||||||
|
duplicate_prefix = Prefix(vrf=vrf, prefix=netaddr.IPNetwork('192.0.2.0/24'))
|
||||||
|
self.assertIsNone(duplicate_prefix.clean())
|
||||||
|
|
||||||
|
def test_duplicate_vrf_unique(self):
|
||||||
|
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=True)
|
||||||
|
Prefix.objects.create(vrf=vrf, prefix=netaddr.IPNetwork('192.0.2.0/24'))
|
||||||
|
duplicate_prefix = Prefix(vrf=vrf, prefix=netaddr.IPNetwork('192.0.2.0/24'))
|
||||||
|
self.assertRaises(ValidationError, duplicate_prefix.clean)
|
||||||
|
|
||||||
|
|
||||||
|
class TestIPAddress(TestCase):
|
||||||
|
|
||||||
|
@override_settings(ENFORCE_GLOBAL_UNIQUE=False)
|
||||||
|
def test_duplicate_global(self):
|
||||||
|
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
|
duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
|
self.assertIsNone(duplicate_ip.clean())
|
||||||
|
|
||||||
|
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
|
||||||
|
def test_duplicate_global_unique(self):
|
||||||
|
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
|
duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
|
self.assertRaises(ValidationError, duplicate_ip.clean)
|
||||||
|
|
||||||
|
def test_duplicate_vrf(self):
|
||||||
|
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=False)
|
||||||
|
IPAddress.objects.create(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
|
duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
|
self.assertIsNone(duplicate_ip.clean())
|
||||||
|
|
||||||
|
def test_duplicate_vrf_unique(self):
|
||||||
|
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=True)
|
||||||
|
IPAddress.objects.create(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
|
duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
|
||||||
|
self.assertRaises(ValidationError, duplicate_ip.clean)
|
||||||
@@ -95,15 +95,16 @@ class VRFListView(ObjectListView):
|
|||||||
filter = filters.VRFFilter
|
filter = filters.VRFFilter
|
||||||
filter_form = forms.VRFFilterForm
|
filter_form = forms.VRFFilterForm
|
||||||
table = tables.VRFTable
|
table = tables.VRFTable
|
||||||
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
|
|
||||||
template_name = 'ipam/vrf_list.html'
|
template_name = 'ipam/vrf_list.html'
|
||||||
|
|
||||||
|
|
||||||
def vrf(request, pk):
|
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)
|
prefix_table = tables.PrefixBriefTable(
|
||||||
prefix_table = tables.PrefixBriefTable(prefixes)
|
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
|
||||||
|
)
|
||||||
|
prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
return render(request, 'ipam/vrf.html', {
|
return render(request, 'ipam/vrf.html', {
|
||||||
'vrf': vrf,
|
'vrf': vrf,
|
||||||
@@ -116,13 +117,13 @@ class VRFEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model = VRF
|
model = VRF
|
||||||
form_class = forms.VRFForm
|
form_class = forms.VRFForm
|
||||||
template_name = 'ipam/vrf_edit.html'
|
template_name = 'ipam/vrf_edit.html'
|
||||||
obj_list_url = 'ipam:vrf_list'
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_vrf'
|
permission_required = 'ipam.delete_vrf'
|
||||||
model = VRF
|
model = VRF
|
||||||
redirect_url = 'ipam:vrf_list'
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -130,21 +131,23 @@ class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.VRFImportForm
|
form = forms.VRFImportForm
|
||||||
table = tables.VRFTable
|
table = tables.VRFTable
|
||||||
template_name = 'ipam/vrf_import.html'
|
template_name = 'ipam/vrf_import.html'
|
||||||
obj_list_url = 'ipam:vrf_list'
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_vrf'
|
permission_required = 'ipam.change_vrf'
|
||||||
cls = VRF
|
cls = VRF
|
||||||
|
filter = filters.VRFFilter
|
||||||
form = forms.VRFBulkEditForm
|
form = forms.VRFBulkEditForm
|
||||||
template_name = 'ipam/vrf_bulk_edit.html'
|
template_name = 'ipam/vrf_bulk_edit.html'
|
||||||
default_redirect_url = 'ipam:vrf_list'
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_vrf'
|
permission_required = 'ipam.delete_vrf'
|
||||||
cls = VRF
|
cls = VRF
|
||||||
default_redirect_url = 'ipam:vrf_list'
|
filter = filters.VRFFilter
|
||||||
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -156,7 +159,6 @@ class RIRListView(ObjectListView):
|
|||||||
filter = filters.RIRFilter
|
filter = filters.RIRFilter
|
||||||
filter_form = forms.RIRFilterForm
|
filter_form = forms.RIRFilterForm
|
||||||
table = tables.RIRTable
|
table = tables.RIRTable
|
||||||
edit_permissions = ['ipam.change_rir', 'ipam.delete_rir']
|
|
||||||
template_name = 'ipam/rir_list.html'
|
template_name = 'ipam/rir_list.html'
|
||||||
|
|
||||||
def alter_queryset(self, request):
|
def alter_queryset(self, request):
|
||||||
@@ -240,14 +242,16 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'ipam.change_rir'
|
permission_required = 'ipam.change_rir'
|
||||||
model = RIR
|
model = RIR
|
||||||
form_class = forms.RIRForm
|
form_class = forms.RIRForm
|
||||||
obj_list_url = 'ipam:rir_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('ipam:rir_list')
|
||||||
|
|
||||||
|
|
||||||
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_rir'
|
permission_required = 'ipam.delete_rir'
|
||||||
cls = RIR
|
cls = RIR
|
||||||
default_redirect_url = 'ipam:rir_list'
|
filter = filters.RIRFilter
|
||||||
|
default_return_url = 'ipam:rir_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -261,7 +265,6 @@ class AggregateListView(ObjectListView):
|
|||||||
filter = filters.AggregateFilter
|
filter = filters.AggregateFilter
|
||||||
filter_form = forms.AggregateFilterForm
|
filter_form = forms.AggregateFilterForm
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateTable
|
||||||
edit_permissions = ['ipam.change_aggregate', 'ipam.delete_aggregate']
|
|
||||||
template_name = 'ipam/aggregate_list.html'
|
template_name = 'ipam/aggregate_list.html'
|
||||||
|
|
||||||
def extra_context(self):
|
def extra_context(self):
|
||||||
@@ -294,9 +297,17 @@ def aggregate(request, pk):
|
|||||||
prefix_table.base_columns['pk'].visible = True
|
prefix_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
||||||
|
|
||||||
|
# Compile permissions list for rendering the object table
|
||||||
|
permissions = {
|
||||||
|
'add': request.user.has_perm('ipam.add_prefix'),
|
||||||
|
'change': request.user.has_perm('ipam.change_prefix'),
|
||||||
|
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, 'ipam/aggregate.html', {
|
return render(request, 'ipam/aggregate.html', {
|
||||||
'aggregate': aggregate,
|
'aggregate': aggregate,
|
||||||
'prefix_table': prefix_table,
|
'prefix_table': prefix_table,
|
||||||
|
'permissions': permissions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -305,13 +316,13 @@ class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model = Aggregate
|
model = Aggregate
|
||||||
form_class = forms.AggregateForm
|
form_class = forms.AggregateForm
|
||||||
template_name = 'ipam/aggregate_edit.html'
|
template_name = 'ipam/aggregate_edit.html'
|
||||||
obj_list_url = 'ipam:aggregate_list'
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_aggregate'
|
permission_required = 'ipam.delete_aggregate'
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
redirect_url = 'ipam:aggregate_list'
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -319,21 +330,23 @@ class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.AggregateImportForm
|
form = forms.AggregateImportForm
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateTable
|
||||||
template_name = 'ipam/aggregate_import.html'
|
template_name = 'ipam/aggregate_import.html'
|
||||||
obj_list_url = 'ipam:aggregate_list'
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_aggregate'
|
permission_required = 'ipam.change_aggregate'
|
||||||
cls = Aggregate
|
cls = Aggregate
|
||||||
|
filter = filters.AggregateFilter
|
||||||
form = forms.AggregateBulkEditForm
|
form = forms.AggregateBulkEditForm
|
||||||
template_name = 'ipam/aggregate_bulk_edit.html'
|
template_name = 'ipam/aggregate_bulk_edit.html'
|
||||||
default_redirect_url = 'ipam:aggregate_list'
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_aggregate'
|
permission_required = 'ipam.delete_aggregate'
|
||||||
cls = Aggregate
|
cls = Aggregate
|
||||||
default_redirect_url = 'ipam:aggregate_list'
|
filter = filters.AggregateFilter
|
||||||
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -343,7 +356,6 @@ class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
class RoleListView(ObjectListView):
|
class RoleListView(ObjectListView):
|
||||||
queryset = Role.objects.all()
|
queryset = Role.objects.all()
|
||||||
table = tables.RoleTable
|
table = tables.RoleTable
|
||||||
edit_permissions = ['ipam.change_role', 'ipam.delete_role']
|
|
||||||
template_name = 'ipam/role_list.html'
|
template_name = 'ipam/role_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -351,14 +363,15 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'ipam.change_role'
|
permission_required = 'ipam.change_role'
|
||||||
model = Role
|
model = Role
|
||||||
form_class = forms.RoleForm
|
form_class = forms.RoleForm
|
||||||
obj_list_url = 'ipam:role_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('ipam:role_list')
|
||||||
|
|
||||||
|
|
||||||
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_role'
|
permission_required = 'ipam.delete_role'
|
||||||
cls = Role
|
cls = Role
|
||||||
default_redirect_url = 'ipam:role_list'
|
default_return_url = 'ipam:role_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -370,7 +383,6 @@ class PrefixListView(ObjectListView):
|
|||||||
filter = filters.PrefixFilter
|
filter = filters.PrefixFilter
|
||||||
filter_form = forms.PrefixFilterForm
|
filter_form = forms.PrefixFilterForm
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
edit_permissions = ['ipam.change_prefix', 'ipam.delete_prefix']
|
|
||||||
template_name = 'ipam/prefix_list.html'
|
template_name = 'ipam/prefix_list.html'
|
||||||
|
|
||||||
def alter_queryset(self, request):
|
def alter_queryset(self, request):
|
||||||
@@ -381,7 +393,9 @@ class PrefixListView(ObjectListView):
|
|||||||
|
|
||||||
def prefix(request, pk):
|
def prefix(request, pk):
|
||||||
|
|
||||||
prefix = get_object_or_404(Prefix.objects.select_related('site', 'vlan', 'role'), pk=pk)
|
prefix = get_object_or_404(Prefix.objects.select_related(
|
||||||
|
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
|
||||||
|
), pk=pk)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
|
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
|
||||||
@@ -397,11 +411,13 @@ def prefix(request, pk):
|
|||||||
.filter(prefix__net_contains=str(prefix.prefix))\
|
.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)
|
||||||
|
parent_prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
# Duplicate prefixes table
|
# Duplicate prefixes table
|
||||||
duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
|
duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
|
||||||
.select_related('site', 'role')
|
.select_related('site', 'role')
|
||||||
duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes)
|
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
|
||||||
|
duplicate_prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
# Child prefixes table
|
# Child prefixes table
|
||||||
if prefix.vrf:
|
if prefix.vrf:
|
||||||
@@ -419,6 +435,13 @@ def prefix(request, pk):
|
|||||||
child_prefix_table.base_columns['pk'].visible = True
|
child_prefix_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
||||||
|
|
||||||
|
# Compile permissions list for rendering the object table
|
||||||
|
permissions = {
|
||||||
|
'add': request.user.has_perm('ipam.add_prefix'),
|
||||||
|
'change': request.user.has_perm('ipam.change_prefix'),
|
||||||
|
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, 'ipam/prefix.html', {
|
return render(request, 'ipam/prefix.html', {
|
||||||
'prefix': prefix,
|
'prefix': prefix,
|
||||||
'aggregate': aggregate,
|
'aggregate': aggregate,
|
||||||
@@ -426,6 +449,8 @@ def prefix(request, pk):
|
|||||||
'parent_prefix_table': parent_prefix_table,
|
'parent_prefix_table': parent_prefix_table,
|
||||||
'child_prefix_table': child_prefix_table,
|
'child_prefix_table': child_prefix_table,
|
||||||
'duplicate_prefix_table': duplicate_prefix_table,
|
'duplicate_prefix_table': duplicate_prefix_table,
|
||||||
|
'permissions': permissions,
|
||||||
|
'return_url': prefix.get_absolute_url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -435,13 +460,14 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
form_class = forms.PrefixForm
|
form_class = forms.PrefixForm
|
||||||
template_name = 'ipam/prefix_edit.html'
|
template_name = 'ipam/prefix_edit.html'
|
||||||
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
|
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
|
||||||
obj_list_url = 'ipam:prefix_list'
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_prefix'
|
permission_required = 'ipam.delete_prefix'
|
||||||
model = Prefix
|
model = Prefix
|
||||||
redirect_url = 'ipam:prefix_list'
|
template_name = 'ipam/prefix_delete.html'
|
||||||
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -449,21 +475,23 @@ class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.PrefixImportForm
|
form = forms.PrefixImportForm
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
template_name = 'ipam/prefix_import.html'
|
template_name = 'ipam/prefix_import.html'
|
||||||
obj_list_url = 'ipam:prefix_list'
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_prefix'
|
permission_required = 'ipam.change_prefix'
|
||||||
cls = Prefix
|
cls = Prefix
|
||||||
|
filter = filters.PrefixFilter
|
||||||
form = forms.PrefixBulkEditForm
|
form = forms.PrefixBulkEditForm
|
||||||
template_name = 'ipam/prefix_bulk_edit.html'
|
template_name = 'ipam/prefix_bulk_edit.html'
|
||||||
default_redirect_url = 'ipam:prefix_list'
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_prefix'
|
permission_required = 'ipam.delete_prefix'
|
||||||
cls = Prefix
|
cls = Prefix
|
||||||
default_redirect_url = 'ipam:prefix_list'
|
filter = filters.PrefixFilter
|
||||||
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
def prefix_ipaddresses(request, pk):
|
def prefix_ipaddresses(request, pk):
|
||||||
@@ -480,9 +508,17 @@ def prefix_ipaddresses(request, pk):
|
|||||||
ip_table.base_columns['pk'].visible = True
|
ip_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
||||||
|
|
||||||
|
# Compile permissions list for rendering the object table
|
||||||
|
permissions = {
|
||||||
|
'add': request.user.has_perm('ipam.add_ipaddress'),
|
||||||
|
'change': request.user.has_perm('ipam.change_ipaddress'),
|
||||||
|
'delete': request.user.has_perm('ipam.delete_ipaddress'),
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, 'ipam/prefix_ipaddresses.html', {
|
return render(request, 'ipam/prefix_ipaddresses.html', {
|
||||||
'prefix': prefix,
|
'prefix': prefix,
|
||||||
'ip_table': ip_table,
|
'ip_table': ip_table,
|
||||||
|
'permissions': permissions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -495,7 +531,6 @@ class IPAddressListView(ObjectListView):
|
|||||||
filter = filters.IPAddressFilter
|
filter = filters.IPAddressFilter
|
||||||
filter_form = forms.IPAddressFilterForm
|
filter_form = forms.IPAddressFilterForm
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
edit_permissions = ['ipam.change_ipaddress', 'ipam.delete_ipaddress']
|
|
||||||
template_name = 'ipam/ipaddress_list.html'
|
template_name = 'ipam/ipaddress_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -504,18 +539,20 @@ def ipaddress(request, pk):
|
|||||||
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
|
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
|
||||||
|
|
||||||
# Parent prefixes table
|
# Parent prefixes table
|
||||||
parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))
|
parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\
|
||||||
parent_prefixes_table = tables.PrefixBriefTable(parent_prefixes)
|
.select_related('site', 'role')
|
||||||
|
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
|
||||||
|
parent_prefixes_table.exclude = ('vrf',)
|
||||||
|
|
||||||
# Duplicate IPs table
|
# Duplicate IPs table
|
||||||
duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\
|
duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\
|
||||||
.exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside')
|
.exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside')
|
||||||
duplicate_ips_table = tables.IPAddressBriefTable(duplicate_ips)
|
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
|
||||||
|
|
||||||
# Related IP table
|
# Related IP table
|
||||||
related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\
|
related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\
|
||||||
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
||||||
related_ips_table = tables.IPAddressBriefTable(related_ips)
|
related_ips_table = tables.IPAddressBriefTable(list(related_ips))
|
||||||
|
|
||||||
return render(request, 'ipam/ipaddress.html', {
|
return render(request, 'ipam/ipaddress.html', {
|
||||||
'ipaddress': ipaddress,
|
'ipaddress': ipaddress,
|
||||||
@@ -548,6 +585,8 @@ def ipaddress_assign(request, pk):
|
|||||||
device.save()
|
device.save()
|
||||||
|
|
||||||
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
||||||
|
else:
|
||||||
|
assert False, form.errors
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form = forms.IPAddressAssignForm()
|
form = forms.IPAddressAssignForm()
|
||||||
@@ -555,7 +594,7 @@ def ipaddress_assign(request, pk):
|
|||||||
return render(request, 'ipam/ipaddress_assign.html', {
|
return render(request, 'ipam/ipaddress_assign.html', {
|
||||||
'ipaddress': ipaddress,
|
'ipaddress': ipaddress,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
|
'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -588,7 +627,7 @@ def ipaddress_remove(request, pk):
|
|||||||
return render(request, 'ipam/ipaddress_unassign.html', {
|
return render(request, 'ipam/ipaddress_unassign.html', {
|
||||||
'ipaddress': ipaddress,
|
'ipaddress': ipaddress,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
|
'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -598,13 +637,13 @@ class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
form_class = forms.IPAddressForm
|
form_class = forms.IPAddressForm
|
||||||
fields_initial = ['address', 'vrf']
|
fields_initial = ['address', 'vrf']
|
||||||
template_name = 'ipam/ipaddress_edit.html'
|
template_name = 'ipam/ipaddress_edit.html'
|
||||||
obj_list_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_ipaddress'
|
permission_required = 'ipam.delete_ipaddress'
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
redirect_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
||||||
@@ -612,7 +651,7 @@ class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
|||||||
form = forms.IPAddressBulkAddForm
|
form = forms.IPAddressBulkAddForm
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
template_name = 'ipam/ipaddress_bulk_add.html'
|
template_name = 'ipam/ipaddress_bulk_add.html'
|
||||||
redirect_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -620,20 +659,18 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.IPAddressImportForm
|
form = forms.IPAddressImportForm
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
template_name = 'ipam/ipaddress_import.html'
|
template_name = 'ipam/ipaddress_import.html'
|
||||||
obj_list_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
def save_obj(self, obj):
|
def save_obj(self, obj):
|
||||||
obj.save()
|
obj.save()
|
||||||
# Update primary IP for device if needed
|
|
||||||
|
# Update primary IP for device if needed. The Device must be updated directly in the database; otherwise we risk
|
||||||
|
# overwriting a previous IP assignment from the same import (see #861).
|
||||||
try:
|
try:
|
||||||
if obj.family == 4 and obj.primary_ip4_for:
|
if obj.family == 4 and obj.primary_ip4_for:
|
||||||
device = obj.primary_ip4_for
|
Device.objects.filter(pk=obj.primary_ip4_for.pk).update(primary_ip4=obj)
|
||||||
device.primary_ip4 = obj
|
|
||||||
device.save()
|
|
||||||
elif obj.family == 6 and obj.primary_ip6_for:
|
elif obj.family == 6 and obj.primary_ip6_for:
|
||||||
device = obj.primary_ip6_for
|
Device.objects.filter(pk=obj.primary_ip6_for.pk).update(primary_ip6=obj)
|
||||||
device.primary_ip6 = obj
|
|
||||||
device.save()
|
|
||||||
except Device.DoesNotExist:
|
except Device.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -641,15 +678,17 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_ipaddress'
|
permission_required = 'ipam.change_ipaddress'
|
||||||
cls = IPAddress
|
cls = IPAddress
|
||||||
|
filter = filters.IPAddressFilter
|
||||||
form = forms.IPAddressBulkEditForm
|
form = forms.IPAddressBulkEditForm
|
||||||
template_name = 'ipam/ipaddress_bulk_edit.html'
|
template_name = 'ipam/ipaddress_bulk_edit.html'
|
||||||
default_redirect_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_ipaddress'
|
permission_required = 'ipam.delete_ipaddress'
|
||||||
cls = IPAddress
|
cls = IPAddress
|
||||||
default_redirect_url = 'ipam:ipaddress_list'
|
filter = filters.IPAddressFilter
|
||||||
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -661,7 +700,6 @@ class VLANGroupListView(ObjectListView):
|
|||||||
filter = filters.VLANGroupFilter
|
filter = filters.VLANGroupFilter
|
||||||
filter_form = forms.VLANGroupFilterForm
|
filter_form = forms.VLANGroupFilterForm
|
||||||
table = tables.VLANGroupTable
|
table = tables.VLANGroupTable
|
||||||
edit_permissions = ['ipam.change_vlangroup', 'ipam.delete_vlangroup']
|
|
||||||
template_name = 'ipam/vlangroup_list.html'
|
template_name = 'ipam/vlangroup_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -669,14 +707,16 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'ipam.change_vlangroup'
|
permission_required = 'ipam.change_vlangroup'
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
form_class = forms.VLANGroupForm
|
form_class = forms.VLANGroupForm
|
||||||
obj_list_url = 'ipam:vlangroup_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('ipam:vlangroup_list')
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_vlangroup'
|
permission_required = 'ipam.delete_vlangroup'
|
||||||
cls = VLANGroup
|
cls = VLANGroup
|
||||||
default_redirect_url = 'ipam:vlangroup_list'
|
filter = filters.VLANGroupFilter
|
||||||
|
default_return_url = 'ipam:vlangroup_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -688,15 +728,15 @@ class VLANListView(ObjectListView):
|
|||||||
filter = filters.VLANFilter
|
filter = filters.VLANFilter
|
||||||
filter_form = forms.VLANFilterForm
|
filter_form = forms.VLANFilterForm
|
||||||
table = tables.VLANTable
|
table = tables.VLANTable
|
||||||
edit_permissions = ['ipam.change_vlan', 'ipam.delete_vlan']
|
|
||||||
template_name = 'ipam/vlan_list.html'
|
template_name = 'ipam/vlan_list.html'
|
||||||
|
|
||||||
|
|
||||||
def vlan(request, pk):
|
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__region', 'tenant__group', 'role'), pk=pk)
|
||||||
prefixes = Prefix.objects.filter(vlan=vlan)
|
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
|
||||||
prefix_table = tables.PrefixBriefTable(prefixes)
|
prefix_table = tables.PrefixBriefTable(list(prefixes))
|
||||||
|
prefix_table.exclude = ('vlan',)
|
||||||
|
|
||||||
return render(request, 'ipam/vlan.html', {
|
return render(request, 'ipam/vlan.html', {
|
||||||
'vlan': vlan,
|
'vlan': vlan,
|
||||||
@@ -709,13 +749,13 @@ class VLANEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model = VLAN
|
model = VLAN
|
||||||
form_class = forms.VLANForm
|
form_class = forms.VLANForm
|
||||||
template_name = 'ipam/vlan_edit.html'
|
template_name = 'ipam/vlan_edit.html'
|
||||||
obj_list_url = 'ipam:vlan_list'
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_vlan'
|
permission_required = 'ipam.delete_vlan'
|
||||||
model = VLAN
|
model = VLAN
|
||||||
redirect_url = 'ipam:vlan_list'
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
@@ -723,21 +763,23 @@ class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
form = forms.VLANImportForm
|
form = forms.VLANImportForm
|
||||||
table = tables.VLANTable
|
table = tables.VLANTable
|
||||||
template_name = 'ipam/vlan_import.html'
|
template_name = 'ipam/vlan_import.html'
|
||||||
obj_list_url = 'ipam:vlan_list'
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_vlan'
|
permission_required = 'ipam.change_vlan'
|
||||||
cls = VLAN
|
cls = VLAN
|
||||||
|
filter = filters.VLANFilter
|
||||||
form = forms.VLANBulkEditForm
|
form = forms.VLANBulkEditForm
|
||||||
template_name = 'ipam/vlan_bulk_edit.html'
|
template_name = 'ipam/vlan_bulk_edit.html'
|
||||||
default_redirect_url = 'ipam:vlan_list'
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_vlan'
|
permission_required = 'ipam.delete_vlan'
|
||||||
cls = VLAN
|
cls = VLAN
|
||||||
default_redirect_url = 'ipam:vlan_list'
|
filter = filters.VLANFilter
|
||||||
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -750,11 +792,14 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
form_class = forms.ServiceForm
|
form_class = forms.ServiceForm
|
||||||
template_name = 'ipam/service_edit.html'
|
template_name = 'ipam/service_edit.html'
|
||||||
|
|
||||||
def alter_obj(self, obj, args, kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
if 'device' in kwargs:
|
if 'device' in url_kwargs:
|
||||||
obj.device = get_object_or_404(Device, pk=kwargs['device'])
|
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return obj.device.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_service'
|
permission_required = 'ipam.delete_service'
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.contrib.messages import constants as messages
|
|||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import configuration
|
from netbox import configuration
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per "
|
raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per "
|
||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.8.1'
|
VERSION = '1.9.0-r1'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
@@ -50,7 +50,7 @@ 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
|
||||||
LDAP_IGNORE_CERT_ERRORS = False
|
LDAP_IGNORE_CERT_ERRORS = False
|
||||||
try:
|
try:
|
||||||
from ldap_config import *
|
from netbox.ldap_config import *
|
||||||
LDAP_CONFIGURED = True
|
LDAP_CONFIGURED = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
LDAP_CONFIGURED = False
|
LDAP_CONFIGURED = False
|
||||||
@@ -104,6 +104,7 @@ INSTALLED_APPS = (
|
|||||||
'django.contrib.humanize',
|
'django.contrib.humanize',
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
'django_tables2',
|
'django_tables2',
|
||||||
|
'mptt',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework_swagger',
|
'rest_framework_swagger',
|
||||||
'circuits',
|
'circuits',
|
||||||
@@ -189,11 +190,6 @@ REST_FRAMEWORK = {
|
|||||||
if LOGIN_REQUIRED:
|
if LOGIN_REQUIRED:
|
||||||
REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ('rest_framework.permissions.IsAuthenticated',)
|
REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ('rest_framework.permissions.IsAuthenticated',)
|
||||||
|
|
||||||
# Swagger settings (API docs)
|
|
||||||
SWAGGER_SETTINGS = {
|
|
||||||
'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Django debug toolbar
|
# Django debug toolbar
|
||||||
INTERNAL_IPS = (
|
INTERNAL_IPS = (
|
||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from django.conf import settings
|
|||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from views import home, handle_500, trigger_500
|
from netbox.views import home, handle_500, trigger_500
|
||||||
from users.views import login, logout
|
from users.views import login, logout
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -264,6 +264,15 @@ ul.rack_far_face li.blocked {
|
|||||||
#ffc7c7 14px
|
#ffc7c7 14px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
ul.rack_near_face li.reserved {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
#f7f7f7,
|
||||||
|
#f7f7f7 7px,
|
||||||
|
#c7c7ff 7px,
|
||||||
|
#c7c7ff 14px
|
||||||
|
);
|
||||||
|
}
|
||||||
ul.rack_near_face {
|
ul.rack_near_face {
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ $(document).ready(function() {
|
|||||||
$('#select_all').prop('checked', false);
|
$('#select_all').prop('checked', false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Enable hidden buttons when "select all" is checked
|
||||||
|
$('#select_all').click(function (event) {
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$('#select_all_box').find('button').prop('disabled', '');
|
||||||
|
} else {
|
||||||
|
$('#select_all_box').find('button').prop('disabled', 'disabled');
|
||||||
|
}
|
||||||
|
});
|
||||||
// Uncheck the "toggle all" checkbox if an item is unchecked
|
// Uncheck the "toggle all" checkbox if an item is unchecked
|
||||||
$('input:checkbox[name=pk]').click(function (event) {
|
$('input:checkbox[name=pk]').click(function (event) {
|
||||||
if (!$(this).attr('checked')) {
|
if (!$(this).attr('checked')) {
|
||||||
@@ -60,37 +68,38 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API select widget
|
// API select widget
|
||||||
$('select[filter-for]').change(function () {
|
$('select[filter-for]').change(function() {
|
||||||
|
|
||||||
// Resolve child field by ID specified in parent
|
// Resolve child field by ID specified in parent
|
||||||
var child_name = $(this).attr('filter-for');
|
var child_name = $(this).attr('filter-for');
|
||||||
var child_field = $('#id_' + child_name);
|
var child_field = $('#id_' + child_name);
|
||||||
|
var child_selected = child_field.val();
|
||||||
|
|
||||||
// Wipe out any existing options within the child field
|
// Wipe out any existing options within the child field and create a default option
|
||||||
child_field.empty();
|
child_field.empty();
|
||||||
child_field.append($("<option></option>").attr("value", "").text(""));
|
child_field.append($("<option></option>").attr("value", "").text("---------"));
|
||||||
|
|
||||||
if ($(this).val()) {
|
|
||||||
|
|
||||||
|
if ($(this).val() || $(this).attr('nullable') == 'true') {
|
||||||
var api_url = child_field.attr('api-url');
|
var api_url = child_field.attr('api-url');
|
||||||
var disabled_indicator = child_field.attr('disabled-indicator');
|
var disabled_indicator = child_field.attr('disabled-indicator');
|
||||||
var initial_value = child_field.attr('initial');
|
var initial_value = child_field.attr('initial');
|
||||||
var display_field = child_field.attr('display-field') || 'name';
|
var display_field = child_field.attr('display-field') || 'name';
|
||||||
|
|
||||||
// Gather the values of all other filter fields for this child
|
// Determine the filter fields needed to make an API call
|
||||||
$("select[filter-for='" + child_name + "']").each(function() {
|
var filter_regex = /\{\{([a-z_]+)\}\}/g;
|
||||||
var filter_field = $(this);
|
var match;
|
||||||
|
while (match = filter_regex.exec(api_url)) {
|
||||||
|
var filter_field = $('#id_' + match[1]);
|
||||||
if (filter_field.val()) {
|
if (filter_field.val()) {
|
||||||
api_url = api_url.replace('{{' + filter_field.attr('name') + '}}', filter_field.val());
|
api_url = api_url.replace(match[0], filter_field.val());
|
||||||
} else {
|
} else if ($(this).attr('nullable') == 'true') {
|
||||||
// Not all filters have been selected yet
|
api_url = api_url.replace(match[0], '0');
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// If all URL variables have been replaced, make the API call
|
// If all URL variables have been replaced, make the API call
|
||||||
if (api_url.search('{{') < 0) {
|
if (api_url.search('{{') < 0) {
|
||||||
|
console.log(child_name + ": Fetching " + api_url);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: api_url,
|
url: api_url,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
@@ -98,7 +107,9 @@ $(document).ready(function() {
|
|||||||
$.each(response, function (index, choice) {
|
$.each(response, function (index, choice) {
|
||||||
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]);
|
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]);
|
||||||
if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) {
|
if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) {
|
||||||
option.attr("disabled", "disabled")
|
option.attr("disabled", "disabled");
|
||||||
|
} else if (choice.id == child_selected) {
|
||||||
|
option.attr("selected", "selected");
|
||||||
}
|
}
|
||||||
child_field.append(option);
|
child_field.append(option);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ $(document).ready(function() {
|
|||||||
$('#generate_keypair').click(function() {
|
$('#generate_keypair').click(function() {
|
||||||
$('#new_keypair_modal').modal('show');
|
$('#new_keypair_modal').modal('show');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/secrets/generate-keys/',
|
url: netbox_api_path + 'secrets/generate-keys/',
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
success: function (response, status) {
|
success: function (response, status) {
|
||||||
@@ -75,7 +75,7 @@ $(document).ready(function() {
|
|||||||
function unlock_secret(secret_id, private_key) {
|
function unlock_secret(secret_id, private_key) {
|
||||||
var csrf_token = $('input[name=csrfmiddlewaretoken]').val();
|
var csrf_token = $('input[name=csrfmiddlewaretoken]').val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/secrets/secrets/' + secret_id + '/',
|
url: netbox_api_path + 'secrets/secrets/' + secret_id + '/',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: {
|
data: {
|
||||||
private_key: private_key
|
private_key: private_key
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from dcim.models import Device
|
|||||||
|
|
||||||
|
|
||||||
class SecretFilter(django_filters.FilterSet):
|
class SecretFilter(django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.CharFilter(
|
||||||
action='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
@@ -33,7 +33,9 @@ class SecretFilter(django_filters.FilterSet):
|
|||||||
model = Secret
|
model = Secret
|
||||||
fields = ['name']
|
fields = ['name']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(device__name__icontains=value)
|
Q(device__name__icontains=value)
|
||||||
|
|||||||
@@ -100,7 +100,11 @@ class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
|
|
||||||
|
|
||||||
class SecretFilterForm(BootstrapMixin, forms.Form):
|
class SecretFilterForm(BootstrapMixin, forms.Form):
|
||||||
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
|
q = forms.CharField(required=False, label='Search')
|
||||||
|
role = FilterChoiceField(
|
||||||
|
queryset=SecretRole.objects.annotate(filter_count=Count('secrets')),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.contrib.auth.models import Group, User
|
|||||||
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 import models
|
from django.db import models
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes, python_2_unicode_compatible
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
@@ -51,6 +51,7 @@ class UserKeyQuerySet(models.QuerySet):
|
|||||||
raise Exception("Bulk deletion has been disabled.")
|
raise Exception("Bulk deletion has been disabled.")
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class UserKey(CreatedUpdatedModel):
|
class UserKey(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted
|
A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted
|
||||||
@@ -76,7 +77,7 @@ class UserKey(CreatedUpdatedModel):
|
|||||||
self.__initial_public_key = self.public_key
|
self.__initial_public_key = self.public_key
|
||||||
self.__initial_master_key_cipher = self.master_key_cipher
|
self.__initial_master_key_cipher = self.master_key_cipher
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.user.username
|
return self.user.username
|
||||||
|
|
||||||
def clean(self, *args, **kwargs):
|
def clean(self, *args, **kwargs):
|
||||||
@@ -170,6 +171,7 @@ class UserKey(CreatedUpdatedModel):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class SecretRole(models.Model):
|
class SecretRole(models.Model):
|
||||||
"""
|
"""
|
||||||
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
|
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
|
||||||
@@ -186,7 +188,7 @@ class SecretRole(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -201,6 +203,7 @@ class SecretRole(models.Model):
|
|||||||
return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists()
|
return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Secret(CreatedUpdatedModel):
|
class Secret(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
|
A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
|
||||||
@@ -227,7 +230,7 @@ class Secret(CreatedUpdatedModel):
|
|||||||
self.plaintext = kwargs.pop('plaintext', None)
|
self.plaintext = kwargs.pop('plaintext', None)
|
||||||
super(Secret, self).__init__(*args, **kwargs)
|
super(Secret, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
if self.role and self.device:
|
if self.role and self.device:
|
||||||
return u'{} for {}'.format(self.role, self.device)
|
return u'{} for {}'.format(self.role, self.device)
|
||||||
return u'Secret'
|
return u'Secret'
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
from test_models import *
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from .models import SecretRole, Secret, UserKey
|
|||||||
class SecretRoleListView(ObjectListView):
|
class SecretRoleListView(ObjectListView):
|
||||||
queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
||||||
table = tables.SecretRoleTable
|
table = tables.SecretRoleTable
|
||||||
edit_permissions = ['secrets.change_secretrole', 'secrets.delete_secretrole']
|
|
||||||
template_name = 'secrets/secretrole_list.html'
|
template_name = 'secrets/secretrole_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -30,14 +29,15 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'secrets.change_secretrole'
|
permission_required = 'secrets.change_secretrole'
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
form_class = forms.SecretRoleForm
|
form_class = forms.SecretRoleForm
|
||||||
obj_list_url = 'secrets:secretrole_list'
|
|
||||||
use_obj_view = False
|
def get_return_url(self, obj):
|
||||||
|
return reverse('secrets:secretrole_list')
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'secrets.delete_secretrole'
|
permission_required = 'secrets.delete_secretrole'
|
||||||
cls = SecretRole
|
cls = SecretRole
|
||||||
default_redirect_url = 'secrets:secretrole_list'
|
default_return_url = 'secrets:secretrole_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -50,7 +50,6 @@ class SecretListView(ObjectListView):
|
|||||||
filter = filters.SecretFilter
|
filter = filters.SecretFilter
|
||||||
filter_form = forms.SecretFilterForm
|
filter_form = forms.SecretFilterForm
|
||||||
table = tables.SecretTable
|
table = tables.SecretTable
|
||||||
edit_permissions = ['secrets.change_secret', 'secrets.delete_secret']
|
|
||||||
template_name = 'secrets/secret_list.html'
|
template_name = 'secrets/secret_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -102,7 +101,7 @@ def secret_add(request, pk):
|
|||||||
return render(request, 'secrets/secret_edit.html', {
|
return render(request, 'secrets/secret_edit.html', {
|
||||||
'secret': secret,
|
'secret': secret,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': device.get_absolute_url(),
|
'return_url': device.get_absolute_url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -144,14 +143,14 @@ def secret_edit(request, pk):
|
|||||||
return render(request, 'secrets/secret_edit.html', {
|
return render(request, 'secrets/secret_edit.html', {
|
||||||
'secret': secret,
|
'secret': secret,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('secrets:secret', kwargs={'pk': secret.pk}),
|
'return_url': reverse('secrets:secret', kwargs={'pk': secret.pk}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'secrets.delete_secret'
|
permission_required = 'secrets.delete_secret'
|
||||||
model = Secret
|
model = Secret
|
||||||
redirect_url = 'secrets:secret_list'
|
default_return_url = 'secrets:secret_list'
|
||||||
|
|
||||||
|
|
||||||
@permission_required('secrets.add_secret')
|
@permission_required('secrets.add_secret')
|
||||||
@@ -194,19 +193,21 @@ def secret_import(request):
|
|||||||
|
|
||||||
return render(request, 'secrets/secret_import.html', {
|
return render(request, 'secrets/secret_import.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': reverse('secrets:secret_list'),
|
'return_url': reverse('secrets:secret_list'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'secrets.change_secret'
|
permission_required = 'secrets.change_secret'
|
||||||
cls = Secret
|
cls = Secret
|
||||||
|
filter = filters.SecretFilter
|
||||||
form = forms.SecretBulkEditForm
|
form = forms.SecretBulkEditForm
|
||||||
template_name = 'secrets/secret_bulk_edit.html'
|
template_name = 'secrets/secret_bulk_edit.html'
|
||||||
default_redirect_url = 'secrets:secret_list'
|
default_return_url = 'secrets:secret_list'
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'secrets.delete_secret'
|
permission_required = 'secrets.delete_secret'
|
||||||
cls = Secret
|
cls = Secret
|
||||||
default_redirect_url = 'secrets:secret_list'
|
filter = filters.SecretFilter
|
||||||
|
default_return_url = 'secrets:secret_list'
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
|
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
<li><a href="{% url 'dcim:region_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Regions</a></li>
|
||||||
|
{% if perms.dcim.add_region %}
|
||||||
|
<li><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Region</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="divider"></li>
|
||||||
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></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 %}
|
{% 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_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>
|
||||||
@@ -296,6 +301,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||||
|
</script>
|
||||||
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
|
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
|
||||||
<script src="{% static 'jquery-ui-1.11.4/jquery-ui.min.js' %}"></script>
|
<script src="{% static 'jquery-ui-1.11.4/jquery-ui.min.js' %}"></script>
|
||||||
<script src="{% static 'bootstrap-3.3.6-dist/js/bootstrap.min.js' %}"></script>
|
<script src="{% static 'bootstrap-3.3.6-dist/js/bootstrap.min.js' %}"></script>
|
||||||
|
|||||||
@@ -66,6 +66,10 @@
|
|||||||
<td>Tenant</td>
|
<td>Tenant</td>
|
||||||
<td>
|
<td>
|
||||||
{% if circuit.tenant %}
|
{% if circuit.tenant %}
|
||||||
|
{% if circuit.tenant.group %}
|
||||||
|
<a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group.name }}</a>
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
|
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
@@ -92,6 +96,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Description</td>
|
||||||
|
<td>
|
||||||
|
{% if circuit.description %}
|
||||||
|
{{ circuit.description }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with circuit.get_custom_fields as custom_fields %}
|
{% with circuit.get_custom_fields as custom_fields %}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
{% render_field form.install_date %}
|
{% render_field form.install_date %}
|
||||||
{% render_field form.commit_rate %}
|
{% render_field form.commit_rate %}
|
||||||
|
{% render_field form.description %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
|
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,10 +58,15 @@
|
|||||||
<td>Commited rate in Kbps (optional)</td>
|
<td>Commited rate in Kbps (optional)</td>
|
||||||
<td>2000</td>
|
<td>2000</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Description</td>
|
||||||
|
<td>Short description (optional)</td>
|
||||||
|
<td>Primary for voice</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<h4>Example</h4>
|
||||||
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000</pre>
|
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/filter_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if termination and perms.circuits.delete_circuittermination %}
|
{% if termination and perms.circuits.delete_circuittermination %}
|
||||||
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}" class="btn btn-xs btn-danger">
|
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}?return_url={{ circuit.get_absolute_url }}" class="btn btn-xs btn-danger">
|
||||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Site</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if termination.site.region %}
|
||||||
|
<a href="{{ termination.site.region.get_absolute_url }}">{{ termination.site.region }}</a>
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a>
|
<a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -34,7 +38,8 @@
|
|||||||
<td>Termination</td>
|
<td>Termination</td>
|
||||||
<td>
|
<td>
|
||||||
{% if termination.interface %}
|
{% if termination.interface %}
|
||||||
<span><a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a> {{ termination.interface }}</span>
|
<a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a>
|
||||||
|
<i class="fa fa-angle-right"></i> {{ termination.interface }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not defined</span>
|
<span class="text-muted">Not defined</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<h1>{{ provider }}</h1>
|
<h1>{{ provider }}</h1>
|
||||||
{% include 'inc/created_updated.html' with obj=provider %}
|
{% include 'inc/created_updated.html' with obj=provider %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Provider</strong>
|
<strong>Provider</strong>
|
||||||
@@ -104,6 +104,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Circuits</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'circuits:circuit_list' %}?provider={{ provider.slug }}">{{ provider.circuits.count }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with provider.get_custom_fields as custom_fields %}
|
{% with provider.get_custom_fields as custom_fields %}
|
||||||
@@ -122,12 +128,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-8">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Circuits</strong>
|
<strong>Circuits</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
|
<tr>
|
||||||
|
<th>Circuit ID</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Tenant</th>
|
||||||
|
<th>A Side</th>
|
||||||
|
<th>Z Side</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
{% for c in circuits %}
|
{% for c in circuits %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -136,6 +150,34 @@
|
|||||||
<td>
|
<td>
|
||||||
<a href="{% url 'circuits:circuit_list' %}?type={{ c.type.slug }}">{{ c.type }}</a>
|
<a href="{% url 'circuits:circuit_list' %}?type={{ c.type.slug }}">{{ c.type }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if c.tenant %}
|
||||||
|
<a href="{% url 'tenancy:tenant' slug=c.tenant.slug %}">{{ c.tenant }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if c.termination_a %}
|
||||||
|
<a href="{% url 'dcim:site' slug=c.termination_a.site.slug %}">{{ c.termination_a.site }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if c.termination_z %}
|
||||||
|
<a href="{% url 'dcim:site' slug=c.termination_z.site.slug %}">{{ c.termination_z.site }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if c.description %}
|
||||||
|
{{ c.description }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
|
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/filter_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
{% render_table table 'table.html' %}
|
{% render_table table 'table.html' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/filter_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3">
|
<div class="col-md-6 col-md-offset-3">
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
@@ -29,6 +32,12 @@
|
|||||||
{% render_field form.livesearch %}
|
{% render_field form.livesearch %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="select">
|
<div class="tab-pane" id="select">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">Site</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ consoleport.device.site }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
{% render_field form.console_server %}
|
{% render_field form.console_server %}
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +49,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-9 col-md-offset-3">
|
<div class="col-md-9 col-md-offset-3">
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
|
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
|
||||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3">
|
<div class="col-md-6 col-md-offset-3">
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
@@ -29,6 +32,12 @@
|
|||||||
{% render_field form.livesearch %}
|
{% render_field form.livesearch %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="select">
|
<div class="tab-pane" id="select">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">Site</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ consoleserverport.device.site }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
{% render_field form.device %}
|
{% render_field form.device %}
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +49,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-9 col-md-offset-3">
|
<div class="col-md-9 col-md-offset-3">
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
|
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
|
||||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% block title %}{{ device }}{% endblock %}
|
{% block title %}{{ device }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'dcim/inc/_device_header.html' with active_tab='info' %}
|
{% include 'dcim/inc/device_header.html' with active_tab='info' %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5 col-lg-6">
|
<div class="col-md-5 col-lg-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -14,26 +14,28 @@
|
|||||||
<strong>Device</strong>
|
<strong>Device</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body attr-table">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<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>
|
||||||
<a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a>
|
{% if device.site.region %}
|
||||||
|
<a href="{{ device.site.region.get_absolute_url }}">{{ device.site.region }}</a>
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Rack</td>
|
<td>Rack</td>
|
||||||
<td>
|
<td>
|
||||||
<span><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}</span>
|
{% if device.rack %}
|
||||||
|
{% if device.rack.group %}
|
||||||
|
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group.name }}</a>
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -44,15 +46,29 @@
|
|||||||
<span>U{{ parent.position }} / {{ parent.get_face_display }}
|
<span>U{{ parent.position }} / {{ parent.get_face_display }}
|
||||||
(<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> - {{ device.parent_bay.name }})</span>
|
(<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> - {{ device.parent_bay.name }})</span>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% elif device.position %}
|
{% elif device.rack and device.position %}
|
||||||
<span>U{{ device.position }} / {{ device.get_face_display }}</span>
|
<span>U{{ device.position }} / {{ device.get_face_display }}</span>
|
||||||
{% elif device.device_type.u_height %}
|
{% elif device.rack and device.device_type.u_height %}
|
||||||
<span class="label label-warning">Not racked</span>
|
<span class="label label-warning">Not racked</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">N/A</span>
|
<span class="text-muted">N/A</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tenant</td>
|
||||||
|
<td>
|
||||||
|
{% if device.tenant %}
|
||||||
|
{% if device.tenant.group %}
|
||||||
|
<a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group.name }}</a>
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Device Type</td>
|
<td>Device Type</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -183,7 +199,7 @@
|
|||||||
{% if ip_addresses %}
|
{% if ip_addresses %}
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for ip in ip_addresses %}
|
{% for ip in ip_addresses %}
|
||||||
{% include 'dcim/inc/_ipaddress.html' %}
|
{% include 'dcim/inc/ipaddress.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
{% elif interfaces or mgmt_interfaces %}
|
{% elif interfaces or mgmt_interfaces %}
|
||||||
@@ -212,7 +228,7 @@
|
|||||||
{% if services %}
|
{% if services %}
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for service in services %}
|
{% for service in services %}
|
||||||
{% include 'dcim/inc/_service.html' %}
|
{% include 'dcim/inc/service.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -234,40 +250,46 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for iface in mgmt_interfaces %}
|
{% for iface in mgmt_interfaces %}
|
||||||
{% include 'dcim/inc/_interface.html' with icon='wrench' %}
|
{% include 'dcim/inc/interface.html' with icon='wrench' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
{% if device.device_type.interface_templates.exists %}
|
||||||
<td colspan="5" class="alert-warning">
|
<tr>
|
||||||
<i class="fa fa-fw fa-warning"></i> No management interfaces defined
|
<td colspan="6" class="alert-warning">
|
||||||
{% if perms.dcim.add_interface %}
|
<i class="fa fa-fw fa-warning"></i> No management interfaces defined
|
||||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
{% if perms.dcim.add_interface %}
|
||||||
{% endif %}
|
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||||
</td>
|
{% endif %}
|
||||||
</tr>
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for cp in console_ports %}
|
{% for cp in console_ports %}
|
||||||
{% include 'dcim/inc/_consoleport.html' %}
|
{% include 'dcim/inc/consoleport.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
{% if device.device_type.console_port_templates.exists %}
|
||||||
<td colspan="5" class="alert-warning">
|
<tr>
|
||||||
<i class="fa fa-fw fa-warning"></i> No console ports defined
|
<td colspan="6" class="alert-warning">
|
||||||
{% if perms.dcim.add_consoleport %}
|
<i class="fa fa-fw fa-warning"></i> No console ports defined
|
||||||
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
{% if perms.dcim.add_consoleport %}
|
||||||
{% endif %}
|
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||||
</td>
|
{% endif %}
|
||||||
</tr>
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for pp in power_ports %}
|
{% for pp in power_ports %}
|
||||||
{% include 'dcim/inc/_powerport.html' %}
|
{% include 'dcim/inc/powerport.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
{% if device.device_type.power_port_templates.exists %}
|
||||||
<td colspan="5" class="alert-warning">
|
<tr>
|
||||||
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
<td colspan="6" class="alert-warning">
|
||||||
{% if perms.dcim.add_powerport %}
|
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
||||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
{% if perms.dcim.add_powerport %}
|
||||||
{% endif %}
|
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||||
</td>
|
{% endif %}
|
||||||
</tr>
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||||
@@ -314,7 +336,11 @@
|
|||||||
<a href="{% url 'dcim:device' pk=rd.pk %}">{{ rd }}</a>
|
<a href="{% url 'dcim:device' pk=rd.pk %}">{{ rd }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:rack' pk=rd.rack.pk %}">Rack {{ rd.rack }}</a>
|
{% if rd.rack %}
|
||||||
|
<a href="{% url 'dcim:rack' pk=rd.rack.pk %}">Rack {{ rd.rack }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ rd.device_type.full_name }}</td>
|
<td>{{ rd.device_type.full_name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -349,7 +375,7 @@
|
|||||||
</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' with selectable=True %}
|
{% 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>
|
||||||
@@ -379,9 +405,10 @@
|
|||||||
{% endif %}
|
{% 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 %}
|
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
@@ -401,7 +428,7 @@
|
|||||||
</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' with selectable=True %}
|
{% 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>
|
||||||
@@ -458,7 +485,7 @@
|
|||||||
</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' with selectable=True %}
|
{% 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>
|
||||||
@@ -510,7 +537,7 @@
|
|||||||
</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' with selectable=True %}
|
{% 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>
|
||||||
@@ -548,9 +575,10 @@
|
|||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function toggleConnection(elem, api_url) {
|
function toggleConnection(elem, api_url) {
|
||||||
|
var url = netbox_api_path + api_url + elem.attr('data') + "/";
|
||||||
if (elem.hasClass('connected')) {
|
if (elem.hasClass('connected')) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: api_url + elem.attr('data') + "/",
|
url: url,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
beforeSend: function(xhr, settings) {
|
beforeSend: function(xhr, settings) {
|
||||||
@@ -569,7 +597,7 @@ function toggleConnection(elem, api_url) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: api_url + elem.attr('data') + "/",
|
url: url,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
beforeSend: function(xhr, settings) {
|
beforeSend: function(xhr, settings) {
|
||||||
@@ -590,13 +618,13 @@ function toggleConnection(elem, api_url) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$(".consoleport-toggle").click(function() {
|
$(".consoleport-toggle").click(function() {
|
||||||
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/console-ports/");
|
return toggleConnection($(this), "dcim/console-ports/");
|
||||||
});
|
});
|
||||||
$(".powerport-toggle").click(function() {
|
$(".powerport-toggle").click(function() {
|
||||||
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/power-ports/");
|
return toggleConnection($(this), "dcim/power-ports/");
|
||||||
});
|
});
|
||||||
$(".interface-toggle").click(function() {
|
$(".interface-toggle").click(function() {
|
||||||
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/interface-connections/");
|
return toggleConnection($(this), "dcim/interface-connections/");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="{% static 'js/graphs.js' %}"></script>
|
<script src="{% static 'js/graphs.js' %}"></script>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<h1>Add {{ component_name|title }}</h1>
|
<h1>Add {{ component_name|title }}</h1>
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if request.POST.redirect_url %}
|
{% if request.POST.return_url %}
|
||||||
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
|
<input type="hidden" name="return_url" value="{{ request.POST.return_url }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for field in form.hidden_fields %}
|
{% for field in form.hidden_fields %}
|
||||||
{{ field }}
|
{{ field }}
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<div class="form-group text-right">
|
<div class="form-group text-right">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
||||||
|
|
||||||
{% block content %}{{ form.errors }}
|
{% block content %}
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<div class="col-md-9 col-md-offset-3">
|
<div class="col-md-9 col-md-offset-3">
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block title %}Device Import{% endblock %}
|
{% block title %}Device Import{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'dcim/inc/_device_import_header.html' %}
|
{% 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">
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
|
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<h4>CSV Format</h4>
|
<h4>CSV Format</h4>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block title %}Device Import{% endblock %}
|
{% block title %}Device Import{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'dcim/inc/_device_import_header.html' with active_tab='child_import' %}
|
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||||
<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">
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
|
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<h4>CSV Format</h4>
|
<h4>CSV Format</h4>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block title %}{{ device }} - Inventory{% endblock %}
|
{% block title %}{{ device }} - Inventory{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'dcim/inc/_device_header.html' with active_tab='inventory' %}
|
{% include 'dcim/inc/device_header.html' with active_tab='inventory' %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<a href="{% url 'dcim:module_edit' pk=m.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:module_edit' pk=m.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_module %}
|
{% if perms.dcim.delete_module %}
|
||||||
<a href="{% url 'dcim:module_delete' pk=m.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:module_delete' pk=m.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -80,10 +80,10 @@
|
|||||||
<td>{{ m2.serial }}</td>
|
<td>{{ m2.serial }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_module %}
|
{% if perms.dcim.change_module %}
|
||||||
<a href="{% url 'dcim:module_edit' pk=m.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:module_edit' pk=m2.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_module %}
|
{% if perms.dcim.delete_module %}
|
||||||
<a href="{% url 'dcim:module_delete' pk=m.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:module_delete' pk=m2.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -96,10 +96,10 @@
|
|||||||
<td>{{ m3.serial }}</td>
|
<td>{{ m3.serial }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_module %}
|
{% if perms.dcim.change_module %}
|
||||||
<a href="{% url 'dcim:module_edit' pk=m.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:module_edit' pk=m3.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_module %}
|
{% if perms.dcim.delete_module %}
|
||||||
<a href="{% url 'dcim:module_delete' pk=m.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:module_delete' pk=m3.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -112,10 +112,10 @@
|
|||||||
<td>{{ m4.serial }}</td>
|
<td>{{ m4.serial }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_module %}
|
{% if perms.dcim.change_module %}
|
||||||
<a href="{% url 'dcim:module_edit' pk=m.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:module_edit' pk=m4.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_module %}
|
{% if perms.dcim.delete_module %}
|
||||||
<a href="{% url 'dcim:module_delete' pk=m.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:module_delete' pk=m4.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -24,7 +24,31 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/filter_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
var model_list = $('#id_device_type_id');
|
||||||
|
$('#id_manufacturer_id').change(function() {
|
||||||
|
model_list.empty();
|
||||||
|
var selected_manufacturers = $(this).val();
|
||||||
|
if (selected_manufacturers) {
|
||||||
|
var api_url = netbox_api_path + 'dcim/device-types/?manufacturer_id=' + selected_manufacturers.join('&manufacturer_id=');
|
||||||
|
$.ajax({
|
||||||
|
url: api_url,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (response, status) {
|
||||||
|
$.each(response, function (index, device_type) {
|
||||||
|
var option = $("<option></option>").attr("value", device_type.id).text(device_type["model"] + " (" + device_type["instance_count"] + ")");
|
||||||
|
model_list.append(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block title %}{{ device }} - LLDP Neighbors{% endblock %}
|
{% block title %}{{ device }} - LLDP Neighbors{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'dcim/inc/_device_header.html' with active_tab='lldp-neighbors' %}
|
{% include 'dcim/inc/device_header.html' with active_tab='lldp-neighbors' %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>LLDP Neighbors</strong>
|
<strong>LLDP Neighbors</strong>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-9 col-md-offset-3">
|
<div class="col-md-9 col-md-offset-3">
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,6 +72,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Interface Ordering</td>
|
||||||
|
<td>{{ devicetype.get_interface_ordering_display }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Instances</td>
|
<td>Instances</td>
|
||||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-9 col-md-offset-3">
|
<div class="col-md-9 col-md-offset-3">
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
{% render_field form.part_number %}
|
{% render_field form.part_number %}
|
||||||
{% render_field form.u_height %}
|
{% render_field form.u_height %}
|
||||||
{% render_field form.is_full_depth %}
|
{% render_field form.is_full_depth %}
|
||||||
|
{% render_field form.interface_ordering %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Function</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
{% render_field form.is_console_server %}
|
{% render_field form.is_console_server %}
|
||||||
{% render_field form.is_pdu %}
|
{% render_field form.is_pdu %}
|
||||||
{% render_field form.is_network_device %}
|
{% render_field form.is_network_device %}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/filter_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<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 %}
|
{% if selectable and perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
|
||||||
<td class="pk">
|
<td class="pk">
|
||||||
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
|
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<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 %}
|
{% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||||
<td class="pk">
|
<td class="pk">
|
||||||
<input name="pk" type="checkbox" value="{{ csp.pk }}" />
|
<input name="pk" type="checkbox" value="{{ csp.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-8 col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
{% if device.rack %}
|
<ol class="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
|
||||||
<li><a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a></li>
|
{% if device.rack %}
|
||||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.rack.site.slug }}">Racks</a></li>
|
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.site.slug }}">Racks</a></li>
|
||||||
<li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li>
|
<li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li>
|
||||||
{% if device.parent_bay %}
|
{% endif %}
|
||||||
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
|
{% if device.parent_bay %}
|
||||||
<li>{{ device.parent_bay.name }}</li>
|
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
|
||||||
{% endif %}
|
<li>{{ device.parent_bay.name }}</li>
|
||||||
<li>{{ device }}</li>
|
{% endif %}
|
||||||
</ol>
|
<li>{{ device }}</li>
|
||||||
{% endif %}
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4 col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||||
@@ -7,12 +7,12 @@
|
|||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}" class="formaction">Console Ports</a></li>{% endif %}
|
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Ports</a></li>{% endif %}
|
||||||
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}" class="formaction">Console Server Ports</a></li>{% endif %}
|
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Server Ports</a></li>{% endif %}
|
||||||
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}" class="formaction">Power Ports</a></li>{% endif %}
|
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}" class="formaction">Power Outlets</a></li>{% endif %}
|
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
|
||||||
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}" class="formaction">Interfaces</a></li>{% endif %}
|
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
|
||||||
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}" class="formaction">Device Bays</a></li>{% endif %}
|
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user