mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-10 22:02:17 -06:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6436d703f5 | ||
|
|
b3243704df | ||
|
|
8bedfcfc64 | ||
|
|
e0aa2c33e9 | ||
|
|
49f268a14c | ||
|
|
2bb0e65aea | ||
|
|
a5d2055c11 | ||
|
|
ffc2c564b8 | ||
|
|
16f222b0ab | ||
|
|
3edf90714a | ||
|
|
4e8fc03c2b | ||
|
|
21fe7c57d8 | ||
|
|
3bcc1429dd | ||
|
|
6b50755a5a | ||
|
|
53998e0fff | ||
|
|
7341ae087c | ||
|
|
9ea8dca4e3 | ||
|
|
5262156e1a | ||
|
|
7ac27b59c6 | ||
|
|
d5ecfe7bef | ||
|
|
e58d1ac87e | ||
|
|
bb653e733c | ||
|
|
95257114df | ||
|
|
935da0d51f | ||
|
|
78ed85943b | ||
|
|
b4a842d9da | ||
|
|
ec0cb7a8bc | ||
|
|
841471104b | ||
|
|
ac71416eb9 | ||
|
|
779d685335 | ||
|
|
4d1e798c56 | ||
|
|
a598035236 | ||
|
|
50395aa821 | ||
|
|
6d9c8fd85b | ||
|
|
c3599bacf2 | ||
|
|
c10481b99d | ||
|
|
1cebc1248b | ||
|
|
c97f7041a7 | ||
|
|
89bfb4f722 | ||
|
|
da3935ff36 | ||
|
|
06810bff91 | ||
|
|
a9af75bbd1 | ||
|
|
be6ef15ffa | ||
|
|
e98f0c39d1 | ||
|
|
5666079d92 | ||
|
|
85f5ba9a25 | ||
|
|
df141a48d9 | ||
|
|
fed6fc131b | ||
|
|
cf49891853 | ||
|
|
de2a894269 | ||
|
|
34d10f8db7 | ||
|
|
68f76465cf | ||
|
|
45d6955260 | ||
|
|
30df060357 | ||
|
|
252be84bf0 | ||
|
|
40ab272995 | ||
|
|
0ec3b5db8b | ||
|
|
5dc9723585 | ||
|
|
50a451eddc | ||
|
|
3f8350b78f | ||
|
|
500a56b869 | ||
|
|
e50b7174bf | ||
|
|
8299c735b1 | ||
|
|
124878ed22 | ||
|
|
d888aa67f9 | ||
|
|
0cb3e1749b | ||
|
|
8ff10d5995 | ||
|
|
a5a7358d26 | ||
|
|
63ac8863f3 | ||
|
|
2047a16a57 | ||
|
|
8d6d55d628 | ||
|
|
9a7dd5ea19 | ||
|
|
30b544a743 | ||
|
|
a0bb7b08bd | ||
|
|
e1d655cb23 | ||
|
|
a1b1e261de | ||
|
|
9d50b78b69 | ||
|
|
00986fd7bf | ||
|
|
2519ebff9d | ||
|
|
c33775d71e | ||
|
|
6b0721cc21 | ||
|
|
d306e76420 | ||
|
|
73cd76932a | ||
|
|
5d19a9f50f | ||
|
|
368c30ef9d | ||
|
|
f77bf72de8 | ||
|
|
f2fbd92f78 | ||
|
|
480134302f | ||
|
|
74cc8c022c | ||
|
|
c6f3b00f0e | ||
|
|
626fbd1d10 | ||
|
|
b8df05cf88 | ||
|
|
e56fc4b1ee |
25
.github/ISSUE_TEMPLATE.md
vendored
25
.github/ISSUE_TEMPLATE.md
vendored
@@ -4,23 +4,30 @@
|
|||||||
remove the "is:open" filter from the search bar to include closed issues.
|
remove the "is:open" filter from the search bar to include closed issues.
|
||||||
|
|
||||||
Check the appropriate type for your issue below by placing an x between the
|
Check the appropriate type for your issue below by placing an x between the
|
||||||
brackets. If none of the below apply, please raise your issue for
|
brackets. For assistance with installation issues, or for any other issues
|
||||||
discussion on our mailing list:
|
other than those listed below, please raise your topic for discussion on
|
||||||
|
our mailing list:
|
||||||
|
|
||||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||||
|
|
||||||
Please note that issues which do not fall under any of the below categories
|
Please note that issues which do not fall under any of the below categories
|
||||||
will be closed.
|
will be closed. Due to an excessive backlog of feature requests, we are
|
||||||
|
not currently accepting any proposals which extend NetBox's feature scope.
|
||||||
|
|
||||||
|
Do not prepend any sort of tag to your issue's title. An administrator will
|
||||||
|
review your issue and assign labels as appropriate.
|
||||||
--->
|
--->
|
||||||
### Issue type
|
### Issue type
|
||||||
[ ] Feature request <!-- Requesting the implementation of a new feature -->
|
[ ] Feature request <!-- An enhancement of existing functionality -->
|
||||||
[ ] Bug report <!-- Reporting unexpected or erroneous behavior -->
|
[ ] Bug report <!-- Unexpected or erroneous behavior -->
|
||||||
[ ] Documentation <!-- Proposing a modification to the documentation -->
|
[ ] Documentation <!-- A modification to the documentation -->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Please describe the environment in which you are running NetBox. (Be sure
|
Please describe the environment in which you are running NetBox. (Be sure
|
||||||
to verify that you are running the latest stable release of NetBox before
|
to verify that you are running the latest stable release of NetBox before
|
||||||
submitting a bug report.)
|
submitting a bug report.) If you are submitting a bug report and have made
|
||||||
|
any changes to the code base, please first validate that your bug can be
|
||||||
|
recreated while running an official release.
|
||||||
-->
|
-->
|
||||||
### Environment
|
### Environment
|
||||||
* Python version: <!-- Example: 3.5.4 -->
|
* Python version: <!-- Example: 3.5.4 -->
|
||||||
@@ -28,8 +35,8 @@
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
BUG REPORTS must include:
|
BUG REPORTS must include:
|
||||||
* A list of the steps needed to reproduce the bug
|
* A list of the steps needed for someone else to reproduce the bug
|
||||||
* A description of the expected behavior
|
* A description of the expected and observed behavior
|
||||||
* Any relevant error messages (screenshots may also help)
|
* Any relevant error messages (screenshots may also help)
|
||||||
|
|
||||||
FEATURE REQUESTS must include:
|
FEATURE REQUESTS must include:
|
||||||
|
|||||||
126
CONTRIBUTING.md
126
CONTRIBUTING.md
@@ -1,8 +1,8 @@
|
|||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
If you encounter any issues installing or using NetBox, try one of the
|
If you encounter any issues installing or using NetBox, try one of the
|
||||||
following resources to get assistance. Please **do not** open a GitHub
|
following resources to get assistance. Please **do not** open a GitHub issue
|
||||||
issue except to report bugs or request features.
|
except to report bugs or request features.
|
||||||
|
|
||||||
### Mailing List
|
### Mailing List
|
||||||
|
|
||||||
@@ -13,35 +13,32 @@ installation. You can find us [here](https://groups.google.com/forum/#!forum/net
|
|||||||
### Freenode IRC
|
### Freenode IRC
|
||||||
|
|
||||||
For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/).
|
For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/).
|
||||||
You can connect to Freenode at irc.freenode.net using an IRC client, or
|
You can connect to Freenode at irc.freenode.net using an IRC client, or you can
|
||||||
you can use their [webchat client](https://webchat.freenode.net/).
|
use their [webchat client](https://webchat.freenode.net/).
|
||||||
|
|
||||||
## Reporting Bugs
|
## Reporting Bugs
|
||||||
|
|
||||||
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
|
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
|
||||||
NetBox. If you're running an older version, it's possible that the bug
|
NetBox. If you're running an older version, it's possible that the bug has
|
||||||
has already been fixed.
|
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
|
||||||
already been reported. If you think you may be experiencing a reported
|
been reported. If you think you may be experiencing a reported issue that
|
||||||
issue that hasn't already been resolved, please click "add a reaction"
|
hasn't already been resolved, please click "add a reaction" in the top right
|
||||||
in the top right corner of the issue and add a thumbs up (+1). You might
|
corner of the issue and add a thumbs up (+1). You mightalso want to add a
|
||||||
also want to add a comment describing how it's affecting your
|
comment describing how it's affecting your installation. This will allow us to
|
||||||
installation. This will allow us to prioritize bugs based on how many
|
prioritize bugs based on how many users are affected.
|
||||||
users are affected.
|
|
||||||
|
|
||||||
* If you haven't found an existing issue that describes your suspected
|
* If you haven't found an existing issue that describes your suspected bug,
|
||||||
bug, please inquire about it on the mailing list. **Do not** file an
|
please inquire about it on the mailing list. **Do not** file an issue until you
|
||||||
issue until you have received confirmation that it is in fact a bug.
|
have received confirmation that it is in fact a bug. Invalid issues are very
|
||||||
Invalid issues are very distracting and slow the pace at which NetBox is
|
distracting and slow the pace at which NetBox is developed.
|
||||||
developed.
|
|
||||||
|
|
||||||
* When submitting an issue, please be as descriptive as possible. Be
|
* When submitting an issue, please be as descriptive as possible. Be sure to
|
||||||
sure to include:
|
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
|
* The exact steps that can be taken to reproduce the issue (if applicable)
|
||||||
applicable)
|
|
||||||
* Any error messages generated
|
* Any error messages generated
|
||||||
* Screenshots (if applicable)
|
* Screenshots (if applicable)
|
||||||
|
|
||||||
@@ -49,71 +46,64 @@ sure to include:
|
|||||||
The issue will be reviewed by a moderator after submission and the appropriate
|
The issue will be reviewed by a moderator after submission and the appropriate
|
||||||
labels will be applied.
|
labels will be applied.
|
||||||
|
|
||||||
* Keep in mind that we prioritize bugs based on their severity and how
|
* Keep in mind that we prioritize bugs based on their severity and how much
|
||||||
much work is required to resolve them. It may take some time for someone
|
work is required to resolve them. It may take some time for someone to address
|
||||||
to address your issue.
|
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
|
||||||
requesting is already listed. (Be sure to search closed issues as well,
|
is already listed. (Be sure to search closed issues as well, since some
|
||||||
since some feature requests are rejected.) If the feature you'd like to
|
feature requests have been rejected.) If the feature you'd like to see has
|
||||||
see has already been requested, click "add a reaction" in the top right
|
already been requested and is open, click "add a reaction" in the top right
|
||||||
corner of the issue and add a thumbs up (+1). This ensures that the
|
corner of the issue and add a thumbs up (+1). This ensures that the issue has
|
||||||
issue has a better chance of making it onto the roadmap. Also feel free
|
a better chance of receiving attention. Also feel free to add a comment with
|
||||||
to add a comment with any additional justification for the feature.
|
any additional justification for the feature. (However, note that comments with
|
||||||
(However, note that comments with no substance other than a "+1" will be
|
no substance other than a "+1" will be deleted. Please use GitHub's reactions
|
||||||
deleted. Please use GitHub's reactions feature to indicate your
|
feature to indicate your support.)
|
||||||
support.)
|
|
||||||
|
|
||||||
* While suggestions for new features are welcome, it's important to
|
* Due to an excessive backlog of feature requests, we are not currently
|
||||||
limit the scope of NetBox's feature set to avoid feature creep. For
|
accepting any proposals which substantially extend NetBox's functionality
|
||||||
example, the following features would be firmly out of scope for NetBox:
|
beyond its current feature set. This includes the introduction of any new views
|
||||||
|
or models which have not already been proposed in an existing feature request.
|
||||||
* Ticket management
|
|
||||||
* Network state monitoring
|
|
||||||
* Acting as a DNS server
|
|
||||||
* Acting as an authentication server
|
|
||||||
|
|
||||||
* Before filing a new feature request, consider raising your idea on the
|
* Before filing a new feature request, consider raising your idea on the
|
||||||
mailing list first. Feedback you receive there will help validate and
|
mailing list first. Feedback you receive there will help validate and shape the
|
||||||
shape the proposed feature before filing a formal issue.
|
proposed feature before filing a formal issue.
|
||||||
|
|
||||||
* Good feature requests are very narrowly defined. Be sure to enumerate
|
* Good feature requests are very narrowly defined. Be sure to thoroughly
|
||||||
specific functionality and data schema. The more effort you put into
|
describe the functionality and data model(s) being proposed. The more effort
|
||||||
writing a feature request, the better its chance is of being
|
you put into writing a feature request, the better its chance is of being
|
||||||
implemented. Overly broad feature requests will be closed.
|
implemented. Overly broad feature requests will be closed.
|
||||||
|
|
||||||
* When submitting a feature request on GitHub, be sure to include the
|
* When submitting a feature request on GitHub, be sure to include the
|
||||||
following:
|
following:
|
||||||
|
|
||||||
* A detailed description of the proposed functionality
|
* A detailed description of the proposed functionality
|
||||||
* A use case for the feature; who would use it and what value it
|
* A use case for the feature; who would use it and what value it would add
|
||||||
would add to NetBox
|
to NetBox
|
||||||
* A rough description of changes necessary to the database schema
|
* A rough description of changes necessary to the database schema (if
|
||||||
(if applicable)
|
applicable)
|
||||||
* Any third-party libraries or other resources which would be
|
* Any third-party libraries or other resources which would be involved
|
||||||
involved
|
|
||||||
|
|
||||||
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title.
|
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue
|
||||||
The issue will be reviewed by a moderator after submission and the appropriate
|
title. The issue will be reviewed by a moderator after submission and the
|
||||||
labels will be applied.
|
appropriate labels will be applied.
|
||||||
|
|
||||||
## Submitting Pull Requests
|
## Submitting Pull Requests
|
||||||
|
|
||||||
* Be sure to open an issue before starting work on a pull request, and
|
* Be sure to open an issue before starting work on a pull request, and discuss
|
||||||
discuss your idea with the NetBox maintainers before beginning work.
|
your idea with the NetBox maintainers before beginning work. This will help
|
||||||
This will help prevent wasting time on something that might we might not
|
prevent wasting time on something that might we might not be able to implement.
|
||||||
be able to implement. When suggesting a new feature, also make sure it
|
When suggesting a new feature, also make sure it won't conflict with any work
|
||||||
won't conflict with any work that's already in progress.
|
that's already in progress.
|
||||||
|
|
||||||
* When submitting a pull request, please be sure to work off of the
|
* When submitting a pull request, please be sure to work off of the `develop`
|
||||||
`develop` branch, rather than `master`. In NetBox, the `develop` branch
|
branch, rather than `master`. The `develop` branch is used for ongoing
|
||||||
is used for ongoing development, while `master` is used for tagging new
|
development, while `master` is used for tagging new stable releases.
|
||||||
stable releases.
|
|
||||||
|
|
||||||
* All code submissions should meet the following criteria (CI will
|
* All code submissions should meet the following criteria (CI will enforce
|
||||||
enforce these checks):
|
these checks):
|
||||||
|
|
||||||
* Python syntax is valid
|
* Python syntax is valid
|
||||||
* All tests pass when run with `./manage.py test`
|
* All tests pass when run with `./manage.py test`
|
||||||
|
|||||||
@@ -82,15 +82,15 @@ $ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/6
|
|||||||
|
|
||||||
### Creating a new site
|
### Creating a new site
|
||||||
|
|
||||||
Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required.
|
Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required. This example includes one non required field, "region."
|
||||||
|
|
||||||
```
|
```
|
||||||
$ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site"}'
|
$ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site", "region": 5}'
|
||||||
{
|
{
|
||||||
"id": 16,
|
"id": 16,
|
||||||
"name": "My New Site",
|
"name": "My New Site",
|
||||||
"slug": "my-new-site",
|
"slug": "my-new-site",
|
||||||
"region": null,
|
"region": 5,
|
||||||
"tenant": null,
|
"tenant": null,
|
||||||
"facility": "",
|
"facility": "",
|
||||||
"asn": null,
|
"asn": null,
|
||||||
@@ -102,6 +102,7 @@ $ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0
|
|||||||
"comments": ""
|
"comments": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
Note that in this example we are creating a site bound to a region with the ID of 5. For write API actions (`POST`, `PUT`, and `PATCH`) the integer ID value is used for `ForeignKey` (related model) relationships, instead of the nested representation that is used in the `GET` (list) action.
|
||||||
|
|
||||||
### Modify an existing site
|
### Modify an existing site
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ $ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc
|
|||||||
Send an authenticated `DELETE` request to the site detail endpoint.
|
Send an authenticated `DELETE` request to the site detail endpoint.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ curl -v X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/
|
$ curl -v -X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/
|
||||||
* Connected to localhost (127.0.0.1) port 8000 (#0)
|
* Connected to localhost (127.0.0.1) port 8000 (#0)
|
||||||
> DELETE /api/dcim/sites/16/ HTTP/1.1
|
> DELETE /api/dcim/sites/16/ HTTP/1.1
|
||||||
> User-Agent: curl/7.35.0
|
> User-Agent: curl/7.35.0
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ The base serializer is used to represent the default view of a model. This inclu
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name.
|
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
|
||||||
|
|
||||||
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
|
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ sudo pip install django-auth-ldap
|
|||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`.
|
Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Complete documentation of all `django-auth-ldap` configuration options is included in the project's [official documentation](http://django-auth-ldap.readthedocs.io/).
|
||||||
|
|
||||||
## General Server Configuration
|
## General Server Configuration
|
||||||
|
|
||||||
@@ -52,6 +52,8 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
|
|||||||
LDAP_IGNORE_CERT_ERRORS = True
|
LDAP_IGNORE_CERT_ERRORS = True
|
||||||
```
|
```
|
||||||
|
|
||||||
|
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
|
||||||
|
|
||||||
## User Authentication
|
## User Authentication
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
@@ -78,8 +80,8 @@ AUTH_LDAP_USER_ATTR_MAP = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
# User Groups for Permissions
|
# User Groups for Permissions
|
||||||
!!! Info
|
!!! info
|
||||||
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
|
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
||||||
|
|||||||
@@ -88,6 +88,13 @@ Resolving deltas: 100% (1495/1495), done.
|
|||||||
Checking connectivity... done.
|
Checking connectivity... done.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Ensure that the media directory (`/opt/netbox/netbox/media/` in this example) and all its subdirectories are writable by the user account as which NetBox runs. If the NetBox process does not have permission to write to this directory, attempts to upload files (e.g. image attachments) will fail. (The appropriate user account will vary by platform.)
|
||||||
|
|
||||||
|
```
|
||||||
|
# chown -R netbox:netbox /opt/netbox/netbox/media/
|
||||||
|
```
|
||||||
|
|
||||||
## Install Python Packages
|
## Install Python Packages
|
||||||
|
|
||||||
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
|
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
|
|
||||||
from circuits import filters
|
from circuits import filters
|
||||||
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
from extras.api.serializers import RenderedGraphSerializer
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
|
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from utilities.forms import (
|
|||||||
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
|
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
|
||||||
SmallTextarea, SlugField,
|
SmallTextarea, SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
|
|
||||||
|
|
||||||
@@ -175,7 +174,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
tenant = FilterChoiceField(
|
tenant = FilterChoiceField(
|
||||||
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
|
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ from django.utils.encoding import python_2_unicode_compatible
|
|||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
from extras.models import CustomFieldModel, CustomFieldValue
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.utils import csv_format
|
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
|
from utilities.utils import csv_format
|
||||||
from .constants import *
|
from .constants import *
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from django.utils.safestring import mark_safe
|
from tenancy.tables import COL_TENANT
|
||||||
|
|
||||||
from utilities.tables import BaseTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
from .models import Circuit, CircuitType, Provider
|
from .models import Circuit, CircuitType, Provider
|
||||||
|
|
||||||
|
|
||||||
CIRCUITTYPE_ACTIONS = """
|
CIRCUITTYPE_ACTIONS = """
|
||||||
{% if perms.circuit.change_circuittype %}
|
{% if perms.circuit.change_circuittype %}
|
||||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
@@ -77,7 +76,7 @@ class CircuitTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
cid = tables.LinkColumn(verbose_name='ID')
|
cid = tables.LinkColumn(verbose_name='ID')
|
||||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
|
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
|
||||||
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
|
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z
|
||||||
|
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
from extras.constants import GRAPH_TYPE_PROVIDER
|
||||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
from extras.models import Graph
|
||||||
from users.models import Token
|
from users.models import Token
|
||||||
from utilities.tests import HttpStatusMixin
|
from utilities.tests import HttpStatusMixin
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.conf.urls import url
|
|||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
app_name = 'circuits'
|
app_name = 'circuits'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from utilities.views import (
|
|||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
from .constants import TERM_SIDE_A, TERM_SIDE_Z
|
||||||
|
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
from ipam.models import IPAddress
|
|
||||||
from circuits.models import Circuit, CircuitTermination
|
from circuits.models import Circuit, CircuitTermination
|
||||||
|
from dcim.constants import (
|
||||||
|
CONNECTION_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
||||||
|
RACK_WIDTH_CHOICES, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||||
|
)
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
RackReservation, RackRole, Region, Site,
|
||||||
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
|
||||||
)
|
)
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
|
from ipam.models import IPAddress
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
@@ -729,6 +733,8 @@ class InventoryItemSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WritableInventoryItemSerializer(ValidatedModelSerializer):
|
class WritableInventoryItemSerializer(ValidatedModelSerializer):
|
||||||
|
# Provide a default value to satisfy UniqueTogetherValidator
|
||||||
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.mixins import ListModelMixin
|
from rest_framework.mixins import ListModelMixin
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
|
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
|
||||||
|
|
||||||
from django.conf import settings
|
from dcim import filters
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
RackReservation, RackRole, Region, Site,
|
RackReservation, RackRole, Region, Site,
|
||||||
)
|
)
|
||||||
from dcim import filters
|
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
from extras.api.serializers import RenderedGraphSerializer
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||||
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin
|
from utilities.api import (
|
||||||
from .exceptions import MissingFilterException
|
IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin,
|
||||||
|
)
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from .exceptions import MissingFilterException
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -256,12 +258,19 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
device.platform
|
device.platform
|
||||||
))
|
))
|
||||||
|
|
||||||
# Check that NAPALM is installed and verify the configured driver
|
# Check that NAPALM is installed
|
||||||
try:
|
try:
|
||||||
import napalm
|
import napalm
|
||||||
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||||
|
|
||||||
|
# TODO: Remove support for NAPALM < 2.0
|
||||||
|
try:
|
||||||
|
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
|
||||||
|
except ImportError:
|
||||||
|
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
|
||||||
|
|
||||||
|
# Validate the configured driver
|
||||||
try:
|
try:
|
||||||
driver = napalm.get_network_driver(device.platform.napalm_driver)
|
driver = napalm.get_network_driver(device.platform.napalm_driver)
|
||||||
except ModuleImportError:
|
except ModuleImportError:
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from netaddr import EUI
|
|
||||||
from netaddr.core import AddrFormatError
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from netaddr import EUI
|
||||||
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableCharFieldFilter, NumericInFilter
|
from utilities.filters import NullableCharFieldFilter, NumericInFilter
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
|
from .constants import (
|
||||||
|
IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, STATUS_CHOICES, VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
|
||||||
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
RackReservation, RackRole, Region, Site,
|
||||||
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RegionFilter(django_filters.FilterSet):
|
class RegionFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
label='Parent region (ID)',
|
label='Parent region (ID)',
|
||||||
@@ -36,6 +41,15 @@ class RegionFilter(django_filters.FilterSet):
|
|||||||
model = Region
|
model = Region
|
||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(slug__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
@@ -162,7 +176,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['serial', 'type', 'width', 'u_height', 'desc_units']
|
fields = ['name', 'serial', 'type', 'width', 'u_height', 'desc_units']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -329,7 +343,7 @@ class DeviceRoleFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ['name', 'slug', 'color']
|
fields = ['name', 'slug', 'color', 'vm_role']
|
||||||
|
|
||||||
|
|
||||||
class PlatformFilter(django_filters.FilterSet):
|
class PlatformFilter(django_filters.FilterSet):
|
||||||
@@ -424,7 +438,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Device model (slug)',
|
label='Device model (slug)',
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=STATUS_CHOICES
|
choices=STATUS_CHOICES,
|
||||||
|
null_value=None
|
||||||
)
|
)
|
||||||
is_full_depth = django_filters.BooleanFilter(
|
is_full_depth = django_filters.BooleanFilter(
|
||||||
name='device_type__is_full_depth',
|
name='device_type__is_full_depth',
|
||||||
@@ -453,7 +468,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['serial']
|
fields = ['serial', 'position']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -598,6 +613,10 @@ class DeviceBayFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemFilter(DeviceComponentFilterSet):
|
class InventoryItemFilter(DeviceComponentFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=InventoryItem.objects.all(),
|
queryset=InventoryItem.objects.all(),
|
||||||
label='Parent inventory item (ID)',
|
label='Parent inventory item (ID)',
|
||||||
@@ -616,7 +635,19 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['name', 'part_id', 'serial', 'discovered']
|
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(part_id__icontains=value) |
|
||||||
|
Q(serial__iexact=value) |
|
||||||
|
Q(asset_tag__iexact=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import EUI, AddrFormatError
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from netaddr import EUI, AddrFormatError
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from mptt.forms import TreeNodeChoiceField
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
from mptt.forms import TreeNodeChoiceField
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
@@ -19,17 +19,19 @@ from utilities.forms import (
|
|||||||
SlugField, FilterTreeNodeMultipleChoiceField,
|
SlugField, FilterTreeNodeMultipleChoiceField,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
|
from .constants import (
|
||||||
|
CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES,
|
||||||
|
RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, STATUS_CHOICES,
|
||||||
|
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
|
||||||
|
)
|
||||||
from .formfields import MACAddressFormField
|
from .formfields import MACAddressFormField
|
||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
|
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
||||||
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface,
|
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
|
||||||
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
|
RackRole, Region, Site,
|
||||||
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
|
|
||||||
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
DEVICE_BY_PK_RE = '{\d+\}'
|
DEVICE_BY_PK_RE = '{\d+\}'
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +81,11 @@ class RegionCSVForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RegionFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
model = Site
|
||||||
|
q = forms.CharField(required=False, label='Search')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
@@ -161,7 +168,7 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
tenant = FilterChoiceField(
|
tenant = FilterChoiceField(
|
||||||
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
|
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -357,17 +364,17 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
group_id = FilterChoiceField(
|
group_id = FilterChoiceField(
|
||||||
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
|
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
|
||||||
label='Rack group',
|
label='Rack group',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
tenant = FilterChoiceField(
|
tenant = FilterChoiceField(
|
||||||
queryset=Tenant.objects.annotate(filter_count=Count('racks')),
|
queryset=Tenant.objects.annotate(filter_count=Count('racks')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
role = FilterChoiceField(
|
role = FilterChoiceField(
|
||||||
queryset=RackRole.objects.annotate(filter_count=Count('racks')),
|
queryset=RackRole.objects.annotate(filter_count=Count('racks')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -409,7 +416,7 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
|
|||||||
group_id = FilterChoiceField(
|
group_id = FilterChoiceField(
|
||||||
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
|
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
|
||||||
label='Rack group',
|
label='Rack group',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1029,7 +1036,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
rack_id = FilterChoiceField(
|
rack_id = FilterChoiceField(
|
||||||
queryset=Rack.objects.annotate(filter_count=Count('devices')),
|
queryset=Rack.objects.annotate(filter_count=Count('devices')),
|
||||||
label='Rack',
|
label='Rack',
|
||||||
null_option=(0, 'None'),
|
null_label='-- None --',
|
||||||
)
|
)
|
||||||
role = FilterChoiceField(
|
role = FilterChoiceField(
|
||||||
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
|
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
|
||||||
@@ -1038,7 +1045,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
tenant = FilterChoiceField(
|
tenant = FilterChoiceField(
|
||||||
queryset=Tenant.objects.annotate(filter_count=Count('devices')),
|
queryset=Tenant.objects.annotate(filter_count=Count('devices')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None'),
|
null_label='-- None --',
|
||||||
)
|
)
|
||||||
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
|
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
|
||||||
device_type_id = FilterChoiceField(
|
device_type_id = FilterChoiceField(
|
||||||
@@ -1050,7 +1057,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
platform = FilterChoiceField(
|
platform = FilterChoiceField(
|
||||||
queryset=Platform.objects.annotate(filter_count=Count('devices')),
|
queryset=Platform.objects.annotate(filter_count=Count('devices')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None'),
|
null_label='-- None --',
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
|
status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
|
||||||
mac_address = forms.CharField(required=False, label='MAC address')
|
mac_address = forms.CharField(required=False, label='MAC address')
|
||||||
@@ -1921,3 +1928,47 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemCSVForm(forms.ModelForm):
|
||||||
|
device = FlexibleModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Device name or ID',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Device not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
manufacturer = forms.ModelChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False,
|
||||||
|
help_text='Manufacturer name',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid manufacturer.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItem
|
||||||
|
fields = ['device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||||
|
part_id = forms.CharField(max_length=50, required=False, label='Part ID')
|
||||||
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['manufacturer', 'part_id', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
model = InventoryItem
|
||||||
|
q = forms.CharField(required=False, label='Search')
|
||||||
|
manufacturer = FilterChoiceField(
|
||||||
|
queryset=Manufacturer.objects.annotate(filter_count=Count('inventory_items')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_label='-- None --'
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from itertools import count, groupby
|
from itertools import count, groupby
|
||||||
|
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -15,9 +13,10 @@ from django.db import models
|
|||||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.models import CustomFieldModel, CustomField, CustomFieldValue, ImageAttachment
|
from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment
|
||||||
from extras.rpc import RPC_CLIENTS
|
from extras.rpc import RPC_CLIENTS
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.fields import ColorField, NullableCharField
|
from utilities.fields import ColorField, NullableCharField
|
||||||
@@ -144,6 +143,11 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
def count_circuits(self):
|
def count_circuits(self):
|
||||||
return Circuit.objects.filter(terminations__site=self).count()
|
return Circuit.objects.filter(terminations__site=self).count()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count_vms(self):
|
||||||
|
from virtualization.models import VirtualMachine
|
||||||
|
return VirtualMachine.objects.filter(cluster__site=self).count()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Racks
|
# Racks
|
||||||
@@ -1090,16 +1094,11 @@ class ConsolePort(models.Model):
|
|||||||
class ConsoleServerPortManager(models.Manager):
|
class ConsoleServerPortManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
# Pad any trailing digits to effect natural sorting
|
||||||
Include the trailing numeric portion of each port name to allow for proper ordering.
|
|
||||||
For example:
|
|
||||||
Port 1, Port 2, Port 3 ... Port 9, Port 10, Port 11 ...
|
|
||||||
Instead of:
|
|
||||||
Port 1, Port 10, Port 11 ... Port 19, Port 2, Port 20 ...
|
|
||||||
"""
|
|
||||||
return super(ConsoleServerPortManager, self).get_queryset().extra(select={
|
return super(ConsoleServerPortManager, self).get_queryset().extra(select={
|
||||||
'name_as_integer': "CAST(substring(dcim_consoleserverport.name FROM '[0-9]+$') AS INTEGER)",
|
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), "
|
||||||
}).order_by('device', 'name_as_integer')
|
"LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))",
|
||||||
|
}).order_by('device', 'name_padded')
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
@@ -1118,6 +1117,17 @@ class ConsoleServerPort(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Check that the parent device's DeviceType is a console server
|
||||||
|
if self.device is None:
|
||||||
|
raise ValidationError("Console server ports must be assigned to devices.")
|
||||||
|
device_type = self.device.device_type
|
||||||
|
if not device_type.is_console_server:
|
||||||
|
raise ValidationError("The {} {} device type not support assignment of console server ports.".format(
|
||||||
|
device_type.manufacturer, device_type
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power ports
|
# Power ports
|
||||||
@@ -1161,9 +1171,10 @@ class PowerPort(models.Model):
|
|||||||
class PowerOutletManager(models.Manager):
|
class PowerOutletManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
# Pad any trailing digits to effect natural sorting
|
||||||
return super(PowerOutletManager, self).get_queryset().extra(select={
|
return super(PowerOutletManager, self).get_queryset().extra(select={
|
||||||
'name_padded': "CONCAT(SUBSTRING(dcim_poweroutlet.name FROM '^[^0-9]+'), "
|
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), "
|
||||||
"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '[0-9\/]+$'), 8, '0'))",
|
"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))",
|
||||||
}).order_by('device', 'name_padded')
|
}).order_by('device', 'name_padded')
|
||||||
|
|
||||||
|
|
||||||
@@ -1183,6 +1194,17 @@ class PowerOutlet(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Check that the parent device's DeviceType is a PDU
|
||||||
|
if self.device is None:
|
||||||
|
raise ValidationError("Power outlets must be assigned to devices.")
|
||||||
|
device_type = self.device.device_type
|
||||||
|
if not device_type.is_pdu:
|
||||||
|
raise ValidationError("The {} {} device type not support assignment of power outlets.".format(
|
||||||
|
device_type.manufacturer, device_type
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interfaces
|
# Interfaces
|
||||||
@@ -1239,6 +1261,14 @@ class Interface(models.Model):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
# Check that the parent device's DeviceType is a network device
|
||||||
|
if self.device is not None:
|
||||||
|
device_type = self.device.device_type
|
||||||
|
if not device_type.is_network_device:
|
||||||
|
raise ValidationError("The {} {} device type not support assignment of network interfaces.".format(
|
||||||
|
device_type.manufacturer, device_type
|
||||||
|
))
|
||||||
|
|
||||||
# An Interface must belong to a Device *or* to a VirtualMachine
|
# An Interface must belong to a Device *or* to a VirtualMachine
|
||||||
if self.device and self.virtual_machine:
|
if self.device and self.virtual_machine:
|
||||||
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
|
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
|
||||||
@@ -1422,9 +1452,24 @@ class InventoryItem(models.Model):
|
|||||||
discovered = models.BooleanField(default=False, verbose_name='Discovered')
|
discovered = models.BooleanField(default=False, verbose_name='Discovered')
|
||||||
description = models.CharField(max_length=100, blank=True)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device__id', 'parent__id', 'name']
|
ordering = ['device__id', 'parent__id', 'name']
|
||||||
unique_together = ['device', 'parent', 'name']
|
unique_together = ['device', 'parent', 'name']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return csv_format([
|
||||||
|
self.device.name or '{' + self.device.pk + '}',
|
||||||
|
self.name,
|
||||||
|
self.manufacturer.name if self.manufacturer else None,
|
||||||
|
self.part_id,
|
||||||
|
self.serial,
|
||||||
|
self.asset_tag,
|
||||||
|
self.description
|
||||||
|
])
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ class InterfaceQuerySet(QuerySet):
|
|||||||
|
|
||||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
||||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
|
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
|
||||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)"
|
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
|
||||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
|
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
|
||||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
|
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
|
||||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
|
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
|
||||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
|
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
|
||||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
|
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ from __future__ import unicode_literals
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
|
from tenancy.tables import COL_TENANT
|
||||||
from utilities.tables import BaseTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
REGION_LINK = """
|
REGION_LINK = """
|
||||||
{% if record.get_children %}
|
{% if record.get_children %}
|
||||||
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
|
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
|
||||||
@@ -141,7 +141,7 @@ class SiteTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn()
|
||||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Site
|
model = Site
|
||||||
@@ -154,11 +154,12 @@ class SiteDetailTable(SiteTable):
|
|||||||
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
||||||
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
||||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
|
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
|
||||||
|
vm_count = tables.Column(accessor=Accessor('count_vms'), orderable=False, verbose_name='VMs')
|
||||||
|
|
||||||
class Meta(SiteTable.Meta):
|
class Meta(SiteTable.Meta):
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||||
'vlan_count', 'circuit_count',
|
'vlan_count', 'circuit_count', 'vm_count',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ class RackTable(BaseTable):
|
|||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn()
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
role = tables.TemplateColumn(RACK_ROLE)
|
role = tables.TemplateColumn(RACK_ROLE)
|
||||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||||
|
|
||||||
@@ -231,7 +232,7 @@ class RackImportTable(BaseTable):
|
|||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
facility_id = tables.Column(verbose_name='Facility ID')
|
facility_id = tables.Column(verbose_name='Facility ID')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
u_height = tables.Column(verbose_name='Height (U)')
|
u_height = tables.Column(verbose_name='Height (U)')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -381,13 +382,17 @@ class PlatformTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
device_count = tables.Column(verbose_name='Devices')
|
device_count = tables.Column(verbose_name='Devices')
|
||||||
|
vm_count = tables.Column(verbose_name='VMs')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
actions = tables.TemplateColumn(
|
||||||
verbose_name='')
|
template_code=PLATFORM_ACTIONS,
|
||||||
|
attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name=''
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ('pk', 'name', 'device_count', 'slug', 'napalm_driver', 'actions')
|
fields = ('pk', 'name', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -398,7 +403,7 @@ class DeviceTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
||||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||||
@@ -425,7 +430,7 @@ class DeviceDetailTable(DeviceTable):
|
|||||||
class DeviceImportTable(BaseTable):
|
class DeviceImportTable(BaseTable):
|
||||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
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')
|
||||||
@@ -523,3 +528,17 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# InventoryItems
|
||||||
|
#
|
||||||
|
|
||||||
|
class InventoryItemTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')])
|
||||||
|
manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer')
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = InventoryItem
|
||||||
|
fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description')
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from dcim.constants import IFACE_FF_LAG, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
Manufacturer, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
|
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
|
||||||
RackReservation, RackRole, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
RackReservation, RackRole, Region, Site,
|
||||||
)
|
)
|
||||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||||
from users.models import Token
|
from users.models import Token
|
||||||
@@ -1432,7 +1432,7 @@ class ConsoleServerPortTest(HttpStatusMixin, APITestCase):
|
|||||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_console_server=True
|
||||||
)
|
)
|
||||||
devicerole = DeviceRole.objects.create(
|
devicerole = DeviceRole.objects.create(
|
||||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||||
@@ -1590,7 +1590,7 @@ class PowerOutletTest(HttpStatusMixin, APITestCase):
|
|||||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_pdu=True
|
||||||
)
|
)
|
||||||
devicerole = DeviceRole.objects.create(
|
devicerole = DeviceRole.objects.create(
|
||||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||||
@@ -1667,7 +1667,7 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
|
|||||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||||
devicetype = DeviceType.objects.create(
|
devicetype = DeviceType.objects.create(
|
||||||
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_network_device=True
|
||||||
)
|
)
|
||||||
devicerole = DeviceRole.objects.create(
|
devicerole = DeviceRole.objects.create(
|
||||||
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ from django.conf.urls import url
|
|||||||
from extras.views import ImageAttachmentEditView
|
from extras.views import ImageAttachmentEditView
|
||||||
from ipam.views import ServiceCreateView
|
from ipam.views import ServiceCreateView
|
||||||
from secrets.views import secret_add
|
from secrets.views import secret_add
|
||||||
from .models import Device, Rack, Site
|
|
||||||
from . import views
|
from . import views
|
||||||
|
from .models import Device, Rack, Site
|
||||||
|
|
||||||
app_name = 'dcim'
|
app_name = 'dcim'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -196,9 +195,13 @@ urlpatterns = [
|
|||||||
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
|
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
|
||||||
|
|
||||||
# Inventory items
|
# Inventory items
|
||||||
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
|
url(r'^inventory-items/$', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
|
||||||
|
url(r'^inventory-items/import/$', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
|
||||||
|
url(r'^inventory-items/edit/$', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
|
||||||
|
url(r'^inventory-items/delete/$', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
|
||||||
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
||||||
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
||||||
|
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
|
||||||
|
|
||||||
# Console/power/interface connections
|
# Console/power/interface connections
|
||||||
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import re
|
|
||||||
from natsort import natsorted
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -15,22 +14,25 @@ from django.utils.html import escape
|
|||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
from natsort import natsorted
|
||||||
|
|
||||||
from ipam.models import Prefix, Service, VLAN
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
|
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
|
||||||
|
from ipam.models import Prefix, Service, VLAN
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView,
|
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView,
|
||||||
ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
|
from virtualization.models import VirtualMachine
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
|
from .constants import CONNECTION_STATUS_CONNECTED
|
||||||
from .models import (
|
from .models import (
|
||||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
RackGroup, RackReservation, RackRole, Region, Site,
|
RackReservation, RackRole, Region, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +80,8 @@ class BulkDisconnectView(View):
|
|||||||
|
|
||||||
class RegionListView(ObjectListView):
|
class RegionListView(ObjectListView):
|
||||||
queryset = Region.objects.annotate(site_count=Count('sites'))
|
queryset = Region.objects.annotate(site_count=Count('sites'))
|
||||||
|
filter = filters.RegionFilter
|
||||||
|
filter_form = forms.RegionFilterForm
|
||||||
table = tables.RegionTable
|
table = tables.RegionTable
|
||||||
template_name = 'dcim/region_list.html'
|
template_name = 'dcim/region_list.html'
|
||||||
|
|
||||||
@@ -133,6 +137,7 @@ class SiteView(View):
|
|||||||
'prefix_count': Prefix.objects.filter(site=site).count(),
|
'prefix_count': Prefix.objects.filter(site=site).count(),
|
||||||
'vlan_count': VLAN.objects.filter(site=site).count(),
|
'vlan_count': VLAN.objects.filter(site=site).count(),
|
||||||
'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
|
'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
|
||||||
|
'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
|
||||||
}
|
}
|
||||||
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
||||||
topology_maps = TopologyMap.objects.filter(site=site)
|
topology_maps = TopologyMap.objects.filter(site=site)
|
||||||
@@ -751,7 +756,10 @@ 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', distinct=True),
|
||||||
|
vm_count=Count('virtual_machines', distinct=True)
|
||||||
|
)
|
||||||
table = tables.PlatformTable
|
table = tables.PlatformTable
|
||||||
template_name = 'dcim/platform_list.html'
|
template_name = 'dcim/platform_list.html'
|
||||||
|
|
||||||
@@ -807,15 +815,11 @@ class DeviceView(View):
|
|||||||
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')
|
||||||
)
|
)
|
||||||
cs_ports = natsorted(
|
cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
|
||||||
ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
|
|
||||||
)
|
|
||||||
power_ports = natsorted(
|
power_ports = natsorted(
|
||||||
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
|
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
|
||||||
)
|
)
|
||||||
power_outlets = natsorted(
|
power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port')
|
||||||
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
|
||||||
)
|
|
||||||
interfaces = Interface.objects.order_naturally(
|
interfaces = Interface.objects.order_naturally(
|
||||||
device.device_type.interface_ordering
|
device.device_type.interface_ordering
|
||||||
).filter(
|
).filter(
|
||||||
@@ -1811,6 +1815,14 @@ class InterfaceConnectionsListView(ObjectListView):
|
|||||||
# Inventory items
|
# Inventory items
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class InventoryItemListView(ObjectListView):
|
||||||
|
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||||
|
filter = filters.InventoryItemFilter
|
||||||
|
filter_form = forms.InventoryItemFilterForm
|
||||||
|
table = tables.InventoryItemTable
|
||||||
|
template_name = 'dcim/inventoryitem_list.html'
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
|
class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
|
||||||
permission_required = 'dcim.change_inventoryitem'
|
permission_required = 'dcim.change_inventoryitem'
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
@@ -1822,8 +1834,40 @@ class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
|
|||||||
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
def get_return_url(self, request, obj):
|
||||||
|
return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk})
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
||||||
permission_required = 'dcim.delete_inventoryitem'
|
permission_required = 'dcim.delete_inventoryitem'
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
|
|
||||||
|
def get_return_url(self, request, obj):
|
||||||
|
return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
|
permission_required = 'dcim.add_inventoryitem'
|
||||||
|
model_form = forms.InventoryItemCSVForm
|
||||||
|
table = tables.InventoryItemTable
|
||||||
|
default_return_url = 'dcim:inventoryitem_list'
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'dcim.change_inventoryitem'
|
||||||
|
cls = InventoryItem
|
||||||
|
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||||
|
filter = filters.InventoryItemFilter
|
||||||
|
table = tables.InventoryItemTable
|
||||||
|
form = forms.InventoryItemBulkEditForm
|
||||||
|
default_return_url = 'dcim:inventoryitem_list'
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_inventoryitem'
|
||||||
|
cls = InventoryItem
|
||||||
|
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
|
||||||
|
table = tables.InventoryItemTable
|
||||||
|
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
||||||
|
default_return_url = 'dcim:inventoryitem_list'
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from datetime import datetime
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from extras.models import (
|
from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT
|
||||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
|
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
|
||||||
)
|
|
||||||
from utilities.api import ValidatedModelSerializer
|
from utilities.api import ValidatedModelSerializer
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +38,15 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
|||||||
# Data validation
|
# Data validation
|
||||||
if value not in [None, '']:
|
if value not in [None, '']:
|
||||||
|
|
||||||
|
# Validate integer
|
||||||
|
if cf.type == CF_TYPE_INTEGER:
|
||||||
|
try:
|
||||||
|
int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(
|
||||||
|
"Invalid value for integer field {}: {}".format(field_name, value)
|
||||||
|
)
|
||||||
|
|
||||||
# Validate boolean
|
# Validate boolean
|
||||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
||||||
from dcim.models import Device, Rack, Site
|
from dcim.models import Device, Rack, Site
|
||||||
from extras.models import (
|
from extras.constants import ACTION_CHOICES, GRAPH_TYPE_CHOICES
|
||||||
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, ReportResult, TopologyMap, UserAction,
|
from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
|
||||||
)
|
|
||||||
from users.api.serializers import NestedUserSerializer
|
from users.api.serializers import NestedUserSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
|
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.decorators import detail_route
|
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from rest_framework.decorators import detail_route
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
|
||||||
|
|
||||||
from extras import filters
|
from extras import filters
|
||||||
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
|
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from .models import CF_TYPE_SELECT, CustomField, Graph, ExportTemplate, TopologyMap, UserAction
|
from .constants import CF_TYPE_SELECT
|
||||||
|
from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilter(django_filters.Filter):
|
class CustomFieldFilter(django_filters.Filter):
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
|
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
|
||||||
from .models import (
|
from .constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
|
||||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue,
|
from .models import CustomField, CustomFieldValue, ImageAttachment
|
||||||
ImageAttachment,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
|
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from django.conf import settings
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
|
|
||||||
|
|
||||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
|
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
|
||||||
|
|
||||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
from ncclient.transport.errors import AuthenticationError
|
|
||||||
from paramiko import AuthenticationException
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from ncclient.transport.errors import AuthenticationError
|
||||||
|
from paramiko import AuthenticationException
|
||||||
|
|
||||||
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
|
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from extras.models import ReportResult
|
|
||||||
from extras.reports import get_reports
|
from extras.reports import get_reports
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# Generated by Django 1.11.4 on 2017-09-26 21:25
|
# Generated by Django 1.11.4 on 2017-09-26 21:25
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import django.contrib.postgres.fields.jsonb
|
import django.contrib.postgres.fields.jsonb
|
||||||
@@ -18,7 +19,7 @@ def verify_postgresql_version(apps, schema_editor):
|
|||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute("SELECT VERSION()")
|
cursor.execute("SELECT VERSION()")
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
pg_version = row[0].split()[1]
|
pg_version = re.match('^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
|
||||||
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
|
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
|
||||||
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
|
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import graphviz
|
|
||||||
|
|
||||||
|
import graphviz
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from collections import OrderedDict
|
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ncclient import manager
|
|
||||||
import paramiko
|
import paramiko
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
from ncclient import manager
|
||||||
|
|
||||||
CONNECT_TIMEOUT = 5 # seconds
|
CONNECT_TIMEOUT = 5 # seconds
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.models import Graph, GRAPH_TYPE_SITE, ExportTemplate
|
from extras.constants import GRAPH_TYPE_SITE
|
||||||
|
from extras.models import Graph, ExportTemplate
|
||||||
from users.models import Token
|
from users.models import Token
|
||||||
from utilities.tests import HttpStatusMixin
|
from utilities.tests import HttpStatusMixin
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
from rest_framework import status
|
from datetime import date
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.models import (
|
from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL
|
||||||
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
|
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
|
||||||
CF_TYPE_SELECT, CF_TYPE_URL,
|
|
||||||
)
|
|
||||||
from users.models import Token
|
from users.models import Token
|
||||||
from utilities.tests import HttpStatusMixin
|
from utilities.tests import HttpStatusMixin
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.conf.urls import url
|
|||||||
|
|
||||||
from extras import views
|
from extras import views
|
||||||
|
|
||||||
|
|
||||||
app_name = 'extras'
|
app_name = 'extras'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@@ -6,10 +7,10 @@ from rest_framework.validators import UniqueTogetherValidator
|
|||||||
|
|
||||||
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
|
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from ipam.models import (
|
from ipam.constants import (
|
||||||
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
|
IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
|
||||||
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
|
||||||
)
|
)
|
||||||
|
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
||||||
from virtualization.api.serializers import NestedVirtualMachineSerializer
|
from virtualization.api.serializers import NestedVirtualMachineSerializer
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
|
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
|
||||||
from ipam import filters
|
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
|
from ipam import filters
|
||||||
|
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
|
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ VLAN_STATUS_CHOICES = (
|
|||||||
(VLAN_STATUS_DEPRECATED, 'Deprecated')
|
(VLAN_STATUS_DEPRECATED, 'Deprecated')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bootstrap CSS classes for various statuses
|
# Bootstrap CSS classes
|
||||||
STATUS_CHOICE_CLASSES = {
|
STATUS_CHOICE_CLASSES = {
|
||||||
0: 'default',
|
0: 'default',
|
||||||
1: 'primary',
|
1: 'primary',
|
||||||
@@ -70,6 +70,16 @@ STATUS_CHOICE_CLASSES = {
|
|||||||
4: 'warning',
|
4: 'warning',
|
||||||
5: 'success',
|
5: 'success',
|
||||||
}
|
}
|
||||||
|
ROLE_CHOICE_CLASSES = {
|
||||||
|
10: 'default',
|
||||||
|
20: 'primary',
|
||||||
|
30: 'warning',
|
||||||
|
40: 'success',
|
||||||
|
41: 'success',
|
||||||
|
42: 'success',
|
||||||
|
43: 'success',
|
||||||
|
44: 'success',
|
||||||
|
}
|
||||||
|
|
||||||
# IP protocols (for services)
|
# IP protocols (for services)
|
||||||
IP_PROTOCOL_TCP = 6
|
IP_PROTOCOL_TCP = 6
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import IPNetwork
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from netaddr import IPNetwork
|
||||||
|
|
||||||
from .formfields import IPFormField
|
from .formfields import IPFormField
|
||||||
from .lookups import (
|
from . import lookups
|
||||||
EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals,
|
|
||||||
NetHost, NetHostContained, NetMaskLength, Regex, StartsWith,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def prefix_validator(prefix):
|
def prefix_validator(prefix):
|
||||||
@@ -58,17 +54,18 @@ class IPNetworkField(BaseIPField):
|
|||||||
return 'cidr'
|
return 'cidr'
|
||||||
|
|
||||||
|
|
||||||
IPNetworkField.register_lookup(EndsWith)
|
IPNetworkField.register_lookup(lookups.IExact)
|
||||||
IPNetworkField.register_lookup(IEndsWith)
|
IPNetworkField.register_lookup(lookups.EndsWith)
|
||||||
IPNetworkField.register_lookup(StartsWith)
|
IPNetworkField.register_lookup(lookups.IEndsWith)
|
||||||
IPNetworkField.register_lookup(IStartsWith)
|
IPNetworkField.register_lookup(lookups.StartsWith)
|
||||||
IPNetworkField.register_lookup(Regex)
|
IPNetworkField.register_lookup(lookups.IStartsWith)
|
||||||
IPNetworkField.register_lookup(IRegex)
|
IPNetworkField.register_lookup(lookups.Regex)
|
||||||
IPNetworkField.register_lookup(NetContained)
|
IPNetworkField.register_lookup(lookups.IRegex)
|
||||||
IPNetworkField.register_lookup(NetContainedOrEqual)
|
IPNetworkField.register_lookup(lookups.NetContained)
|
||||||
IPNetworkField.register_lookup(NetContains)
|
IPNetworkField.register_lookup(lookups.NetContainedOrEqual)
|
||||||
IPNetworkField.register_lookup(NetContainsOrEquals)
|
IPNetworkField.register_lookup(lookups.NetContains)
|
||||||
IPNetworkField.register_lookup(NetMaskLength)
|
IPNetworkField.register_lookup(lookups.NetContainsOrEquals)
|
||||||
|
IPNetworkField.register_lookup(lookups.NetMaskLength)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressField(BaseIPField):
|
class IPAddressField(BaseIPField):
|
||||||
@@ -81,16 +78,17 @@ class IPAddressField(BaseIPField):
|
|||||||
return 'inet'
|
return 'inet'
|
||||||
|
|
||||||
|
|
||||||
IPAddressField.register_lookup(EndsWith)
|
IPAddressField.register_lookup(lookups.IExact)
|
||||||
IPAddressField.register_lookup(IEndsWith)
|
IPAddressField.register_lookup(lookups.EndsWith)
|
||||||
IPAddressField.register_lookup(StartsWith)
|
IPAddressField.register_lookup(lookups.IEndsWith)
|
||||||
IPAddressField.register_lookup(IStartsWith)
|
IPAddressField.register_lookup(lookups.StartsWith)
|
||||||
IPAddressField.register_lookup(Regex)
|
IPAddressField.register_lookup(lookups.IStartsWith)
|
||||||
IPAddressField.register_lookup(IRegex)
|
IPAddressField.register_lookup(lookups.Regex)
|
||||||
IPAddressField.register_lookup(NetContained)
|
IPAddressField.register_lookup(lookups.IRegex)
|
||||||
IPAddressField.register_lookup(NetContainedOrEqual)
|
IPAddressField.register_lookup(lookups.NetContained)
|
||||||
IPAddressField.register_lookup(NetContains)
|
IPAddressField.register_lookup(lookups.NetContainedOrEqual)
|
||||||
IPAddressField.register_lookup(NetContainsOrEquals)
|
IPAddressField.register_lookup(lookups.NetContains)
|
||||||
IPAddressField.register_lookup(NetHost)
|
IPAddressField.register_lookup(lookups.NetContainsOrEquals)
|
||||||
IPAddressField.register_lookup(NetHostContained)
|
IPAddressField.register_lookup(lookups.NetHost)
|
||||||
IPAddressField.register_lookup(NetMaskLength)
|
IPAddressField.register_lookup(lookups.NetHostContained)
|
||||||
|
IPAddressField.register_lookup(lookups.NetMaskLength)
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from netaddr import IPNetwork
|
|
||||||
from netaddr.core import AddrFormatError
|
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
import netaddr
|
||||||
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
from dcim.models import Site, Device, Interface
|
from dcim.models import Site, Device, Interface
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NumericInFilter
|
from utilities.filters import NumericInFilter
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from .models import (
|
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
|
||||||
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
@@ -82,7 +79,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
try:
|
try:
|
||||||
prefix = str(IPNetwork(value.strip()).cidr)
|
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||||
except (AddrFormatError, ValueError):
|
except (AddrFormatError, ValueError):
|
||||||
pass
|
pass
|
||||||
@@ -102,9 +99,22 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
# TODO: Deprecate in v2.3.0
|
||||||
parent = django_filters.CharFilter(
|
parent = django_filters.CharFilter(
|
||||||
method='search_by_parent',
|
method='search_within_include',
|
||||||
label='Parent prefix',
|
label='Parent prefix (deprecated)',
|
||||||
|
)
|
||||||
|
within = django_filters.CharFilter(
|
||||||
|
method='search_within',
|
||||||
|
label='Within prefix',
|
||||||
|
)
|
||||||
|
within_include = django_filters.CharFilter(
|
||||||
|
method='search_within_include',
|
||||||
|
label='Within and including prefix',
|
||||||
|
)
|
||||||
|
contains = django_filters.CharFilter(
|
||||||
|
method='search_contains',
|
||||||
|
label='Prefixes which contain this prefix or IP',
|
||||||
)
|
)
|
||||||
mask_length = django_filters.NumberFilter(
|
mask_length = django_filters.NumberFilter(
|
||||||
method='filter_mask_length',
|
method='filter_mask_length',
|
||||||
@@ -159,7 +169,8 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=PREFIX_STATUS_CHOICES
|
choices=PREFIX_STATUS_CHOICES,
|
||||||
|
null_value=None
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -171,22 +182,46 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
try:
|
try:
|
||||||
prefix = str(IPNetwork(value.strip()).cidr)
|
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||||
except (AddrFormatError, ValueError):
|
except (AddrFormatError, ValueError):
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
def search_by_parent(self, queryset, name, value):
|
def search_within(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(netaddr.IPNetwork(value).cidr)
|
||||||
|
return queryset.filter(prefix__net_contained=query)
|
||||||
|
except (AddrFormatError, ValueError):
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
|
def search_within_include(self, queryset, name, value):
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return queryset
|
||||||
|
try:
|
||||||
|
query = str(netaddr.IPNetwork(value).cidr)
|
||||||
return queryset.filter(prefix__net_contained_or_equal=query)
|
return queryset.filter(prefix__net_contained_or_equal=query)
|
||||||
except (AddrFormatError, ValueError):
|
except (AddrFormatError, ValueError):
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
|
def search_contains(self, queryset, name, value):
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return queryset
|
||||||
|
try:
|
||||||
|
# Searching by prefix
|
||||||
|
if '/' in value:
|
||||||
|
return queryset.filter(prefix__net_contains_or_equals=str(netaddr.IPNetwork(value).cidr))
|
||||||
|
# Searching by IP address
|
||||||
|
else:
|
||||||
|
return queryset.filter(prefix__net_contains=str(netaddr.IPAddress(value)))
|
||||||
|
except (AddrFormatError, ValueError):
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
def filter_mask_length(self, queryset, name, value):
|
def filter_mask_length(self, queryset, name, value):
|
||||||
if not value:
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
@@ -254,7 +289,8 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Interface (ID)',
|
label='Interface (ID)',
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=IPADDRESS_STATUS_CHOICES
|
choices=IPADDRESS_STATUS_CHOICES,
|
||||||
|
null_value=None
|
||||||
)
|
)
|
||||||
role = django_filters.MultipleChoiceFilter(
|
role = django_filters.MultipleChoiceFilter(
|
||||||
choices=IPADDRESS_ROLE_CHOICES
|
choices=IPADDRESS_ROLE_CHOICES
|
||||||
@@ -278,7 +314,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
if not value:
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
try:
|
try:
|
||||||
query = str(IPNetwork(value.strip()).cidr)
|
query = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||||
return queryset.filter(address__net_host_contained=query)
|
return queryset.filter(address__net_host_contained=query)
|
||||||
except (AddrFormatError, ValueError):
|
except (AddrFormatError, ValueError):
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
@@ -353,7 +389,8 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=VLAN_STATUS_CHOICES
|
choices=VLAN_STATUS_CHOICES,
|
||||||
|
null_value=None
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import IPNetwork, AddrFormatError
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from netaddr import IPNetwork, AddrFormatError
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -14,11 +14,8 @@ from utilities.forms import (
|
|||||||
add_blank_choice,
|
add_blank_choice,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from .models import (
|
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
|
||||||
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
IP_FAMILY_CHOICES = [
|
IP_FAMILY_CHOICES = [
|
||||||
('', 'All'),
|
('', 'All'),
|
||||||
@@ -81,8 +78,11 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = VRF
|
model = VRF
|
||||||
q = forms.CharField(required=False, label='Search')
|
q = forms.CharField(required=False, label='Search')
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
|
tenant = FilterChoiceField(
|
||||||
null_option=(0, None))
|
queryset=Tenant.objects.annotate(filter_count=Count('vrfs')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_label='-- None --'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -362,7 +362,7 @@ def prefix_status_choices():
|
|||||||
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
q = forms.CharField(required=False, label='Search')
|
q = forms.CharField(required=False, label='Search')
|
||||||
parent = forms.CharField(required=False, label='Parent prefix', widget=forms.TextInput(attrs={
|
within_include = forms.CharField(required=False, label='Search within', 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')
|
||||||
@@ -371,23 +371,23 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
|
queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
|
||||||
to_field_name='rd',
|
to_field_name='rd',
|
||||||
label='VRF',
|
label='VRF',
|
||||||
null_option=(0, 'Global')
|
null_label='-- Global --'
|
||||||
)
|
)
|
||||||
tenant = FilterChoiceField(
|
tenant = FilterChoiceField(
|
||||||
queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
|
queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
|
status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
|
||||||
site = FilterChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.annotate(filter_count=Count('prefixes')),
|
queryset=Site.objects.annotate(filter_count=Count('prefixes')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
role = FilterChoiceField(
|
role = FilterChoiceField(
|
||||||
queryset=Role.objects.annotate(filter_count=Count('prefixes')),
|
queryset=Role.objects.annotate(filter_count=Count('prefixes')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
||||||
|
|
||||||
@@ -691,6 +691,11 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
nullable_fields = ['vrf', 'role', 'tenant', 'description']
|
nullable_fields = ['vrf', 'role', 'tenant', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||||
|
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
|
||||||
|
address = forms.CharField(label='IP Address')
|
||||||
|
|
||||||
|
|
||||||
def ipaddress_status_choices():
|
def ipaddress_status_choices():
|
||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||||
@@ -717,12 +722,12 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
|
queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
|
||||||
to_field_name='rd',
|
to_field_name='rd',
|
||||||
label='VRF',
|
label='VRF',
|
||||||
null_option=(0, 'Global')
|
null_label='-- Global --'
|
||||||
)
|
)
|
||||||
tenant = FilterChoiceField(
|
tenant = FilterChoiceField(
|
||||||
queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
|
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
|
||||||
role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
|
role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
|
||||||
@@ -764,7 +769,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
|||||||
site = FilterChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
|
queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'Global')
|
null_label='-- Global --'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -894,23 +899,23 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
site = FilterChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.annotate(filter_count=Count('vlans')),
|
queryset=Site.objects.annotate(filter_count=Count('vlans')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'Global')
|
null_label='-- Global --'
|
||||||
)
|
)
|
||||||
group_id = FilterChoiceField(
|
group_id = FilterChoiceField(
|
||||||
queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
|
queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
|
||||||
label='VLAN group',
|
label='VLAN group',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
tenant = FilterChoiceField(
|
tenant = FilterChoiceField(
|
||||||
queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
|
queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
||||||
role = FilterChoiceField(
|
role = FilterChoiceField(
|
||||||
queryset=Role.objects.annotate(filter_count=Count('vlans')),
|
queryset=Role.objects.annotate(filter_count=Count('vlans')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_option=(0, 'None')
|
null_label='-- None --'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,21 @@ class NetFieldDecoratorMixin(object):
|
|||||||
return lhs_string, lhs_params
|
return lhs_string, lhs_params
|
||||||
|
|
||||||
|
|
||||||
|
class IExact(NetFieldDecoratorMixin, lookups.IExact):
|
||||||
|
|
||||||
|
def get_rhs_op(self, connection, rhs):
|
||||||
|
return '= LOWER(%s)' % rhs
|
||||||
|
|
||||||
|
|
||||||
class EndsWith(NetFieldDecoratorMixin, lookups.EndsWith):
|
class EndsWith(NetFieldDecoratorMixin, lookups.EndsWith):
|
||||||
lookup_name = 'endswith'
|
pass
|
||||||
|
|
||||||
|
|
||||||
class IEndsWith(NetFieldDecoratorMixin, lookups.IEndsWith):
|
class IEndsWith(NetFieldDecoratorMixin, lookups.IEndsWith):
|
||||||
lookup_name = 'iendswith'
|
pass
|
||||||
|
|
||||||
|
def get_rhs_op(self, connection, rhs):
|
||||||
|
return 'LIKE LOWER(%s)' % rhs
|
||||||
|
|
||||||
|
|
||||||
class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith):
|
class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith):
|
||||||
@@ -26,15 +35,18 @@ class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith):
|
|||||||
|
|
||||||
|
|
||||||
class IStartsWith(NetFieldDecoratorMixin, lookups.IStartsWith):
|
class IStartsWith(NetFieldDecoratorMixin, lookups.IStartsWith):
|
||||||
lookup_name = 'istartswith'
|
pass
|
||||||
|
|
||||||
|
def get_rhs_op(self, connection, rhs):
|
||||||
|
return 'LIKE LOWER(%s)' % rhs
|
||||||
|
|
||||||
|
|
||||||
class Regex(NetFieldDecoratorMixin, lookups.Regex):
|
class Regex(NetFieldDecoratorMixin, lookups.Regex):
|
||||||
lookup_name = 'regex'
|
pass
|
||||||
|
|
||||||
|
|
||||||
class IRegex(NetFieldDecoratorMixin, lookups.IRegex):
|
class IRegex(NetFieldDecoratorMixin, lookups.IRegex):
|
||||||
lookup_name = 'iregex'
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NetContainsOrEquals(Lookup):
|
class NetContainsOrEquals(Lookup):
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import netaddr
|
|
||||||
|
|
||||||
|
import netaddr
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -282,11 +281,35 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
def get_duplicates(self):
|
def get_duplicates(self):
|
||||||
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
|
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
|
||||||
|
|
||||||
|
def get_child_prefixes(self):
|
||||||
|
"""
|
||||||
|
Return all Prefixes within this Prefix and VRF. If this Prefix is a container in the global table, return child
|
||||||
|
Prefixes belonging to any VRF.
|
||||||
|
"""
|
||||||
|
if self.vrf is None and self.status == PREFIX_STATUS_CONTAINER:
|
||||||
|
return Prefix.objects.filter(prefix__net_contained=str(self.prefix))
|
||||||
|
else:
|
||||||
|
return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
|
||||||
|
|
||||||
def get_child_ips(self):
|
def get_child_ips(self):
|
||||||
"""
|
"""
|
||||||
Return all IPAddresses within this Prefix.
|
Return all IPAddresses within this Prefix and VRF. If this Prefix is a container in the global table, return
|
||||||
|
child IPAddresses belonging to any VRF.
|
||||||
"""
|
"""
|
||||||
return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf)
|
if self.vrf is None and self.status == PREFIX_STATUS_CONTAINER:
|
||||||
|
return IPAddress.objects.filter(address__net_host_contained=str(self.prefix))
|
||||||
|
else:
|
||||||
|
return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf)
|
||||||
|
|
||||||
|
def get_available_prefixes(self):
|
||||||
|
"""
|
||||||
|
Return all available Prefixes within this prefix as an IPSet.
|
||||||
|
"""
|
||||||
|
prefix = netaddr.IPSet(self.prefix)
|
||||||
|
child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()])
|
||||||
|
available_prefixes = prefix - child_prefixes
|
||||||
|
|
||||||
|
return available_prefixes
|
||||||
|
|
||||||
def get_available_ips(self):
|
def get_available_ips(self):
|
||||||
"""
|
"""
|
||||||
@@ -305,6 +328,24 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
return available_ips
|
return available_ips
|
||||||
|
|
||||||
|
def get_first_available_prefix(self):
|
||||||
|
"""
|
||||||
|
Return the first available child prefix within the prefix (or None).
|
||||||
|
"""
|
||||||
|
available_prefixes = self.get_available_prefixes()
|
||||||
|
if not available_prefixes:
|
||||||
|
return None
|
||||||
|
return available_prefixes.iter_cidrs()[0]
|
||||||
|
|
||||||
|
def get_first_available_ip(self):
|
||||||
|
"""
|
||||||
|
Return the first available IP within the prefix (or None).
|
||||||
|
"""
|
||||||
|
available_ips = self.get_available_ips()
|
||||||
|
if not available_ips:
|
||||||
|
return None
|
||||||
|
return '{}/{}'.format(next(available_ips.__iter__()), self.prefix.prefixlen)
|
||||||
|
|
||||||
def get_utilization(self):
|
def get_utilization(self):
|
||||||
"""
|
"""
|
||||||
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
|
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
|
||||||
@@ -315,25 +356,12 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
|
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
|
||||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||||
else:
|
else:
|
||||||
child_count = IPAddress.objects.filter(
|
child_count = self.get_child_ips().count()
|
||||||
address__net_contained_or_equal=str(self.prefix), vrf=self.vrf
|
|
||||||
).count()
|
|
||||||
prefix_size = self.prefix.size
|
prefix_size = self.prefix.size
|
||||||
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
|
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
|
||||||
prefix_size -= 2
|
prefix_size -= 2
|
||||||
return int(float(child_count) / prefix_size * 100)
|
return int(float(child_count) / prefix_size * 100)
|
||||||
|
|
||||||
@property
|
|
||||||
def new_subnet(self):
|
|
||||||
if self.family == 4:
|
|
||||||
if self.prefix.prefixlen <= 30:
|
|
||||||
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
|
|
||||||
return None
|
|
||||||
if self.family == 6:
|
|
||||||
if self.prefix.prefixlen <= 126:
|
|
||||||
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class IPAddressManager(models.Manager):
|
class IPAddressManager(models.Manager):
|
||||||
|
|
||||||
@@ -461,6 +489,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return STATUS_CHOICE_CLASSES[self.status]
|
return STATUS_CHOICE_CLASSES[self.status]
|
||||||
|
|
||||||
|
def get_role_class(self):
|
||||||
|
return ROLE_CHOICE_CLASSES[self.role]
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class VLANGroup(models.Model):
|
class VLANGroup(models.Model):
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ from __future__ import unicode_literals
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
|
from tenancy.tables import COL_TENANT
|
||||||
from utilities.tables import BaseTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
|
|
||||||
RIR_UTILIZATION = """
|
RIR_UTILIZATION = """
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
{% if record.stats.total %}
|
{% if record.stats.total %}
|
||||||
@@ -49,13 +49,7 @@ PREFIX_LINK = """
|
|||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
|
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% if parent.tenant %}&tenant_group={{ parent.tenant.group.pk }}&tenant={{ parent.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
||||||
</span>
|
|
||||||
"""
|
|
||||||
|
|
||||||
PREFIX_LINK_BRIEF = """
|
|
||||||
<span style="padding-left: {{ record.depth }}0px">
|
|
||||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
|
||||||
</span>
|
</span>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -77,6 +71,10 @@ IPADDRESS_LINK = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
IPADDRESS_ASSIGN_LINK = """
|
||||||
|
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
|
||||||
|
"""
|
||||||
|
|
||||||
IPADDRESS_PARENT = """
|
IPADDRESS_PARENT = """
|
||||||
{% if record.interface %}
|
{% if record.interface %}
|
||||||
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
|
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
|
||||||
@@ -134,9 +132,9 @@ VLANGROUP_ACTIONS = """
|
|||||||
|
|
||||||
TENANT_LINK = """
|
TENANT_LINK = """
|
||||||
{% if record.tenant %}
|
{% if record.tenant %}
|
||||||
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}">{{ record.tenant }}</a>
|
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
|
||||||
{% elif record.vrf.tenant %}
|
{% elif record.vrf.tenant %}
|
||||||
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}">{{ record.vrf.tenant }}</a>*
|
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}" title="{{ record.vrf.tenant.description }}">{{ record.vrf.tenant }}</a>*
|
||||||
{% else %}
|
{% else %}
|
||||||
—
|
—
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -151,7 +149,7 @@ class VRFTable(BaseTable):
|
|||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn()
|
||||||
rd = tables.Column(verbose_name='RD')
|
rd = tables.Column(verbose_name='RD')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VRF
|
model = VRF
|
||||||
@@ -242,7 +240,7 @@ class PrefixTable(BaseTable):
|
|||||||
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
|
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
|
||||||
status = tables.TemplateColumn(STATUS_LABEL)
|
status = tables.TemplateColumn(STATUS_LABEL)
|
||||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
tenant = tables.TemplateColumn(template_code=TENANT_LINK)
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
||||||
role = tables.TemplateColumn(PREFIX_ROLE_LINK)
|
role = tables.TemplateColumn(PREFIX_ROLE_LINK)
|
||||||
@@ -269,9 +267,9 @@ class PrefixDetailTable(PrefixTable):
|
|||||||
class IPAddressTable(BaseTable):
|
class IPAddressTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||||
status = tables.TemplateColumn(STATUS_LABEL)
|
|
||||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
status = tables.TemplateColumn(STATUS_LABEL)
|
||||||
|
tenant = tables.TemplateColumn(template_code=TENANT_LINK)
|
||||||
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
|
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
|
||||||
interface = tables.Column(orderable=False)
|
interface = tables.Column(orderable=False)
|
||||||
|
|
||||||
@@ -294,6 +292,18 @@ class IPAddressDetailTable(IPAddressTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IPAddressAssignTable(BaseTable):
|
||||||
|
address = tables.TemplateColumn(IPADDRESS_ASSIGN_LINK, verbose_name='IP Address')
|
||||||
|
status = tables.TemplateColumn(STATUS_LABEL)
|
||||||
|
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
|
||||||
|
interface = tables.Column(orderable=False)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = IPAddress
|
||||||
|
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface')
|
||||||
|
orderable = False
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLAN groups
|
# VLAN groups
|
||||||
#
|
#
|
||||||
@@ -321,7 +331,7 @@ class VLANTable(BaseTable):
|
|||||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
status = tables.TemplateColumn(STATUS_LABEL)
|
status = tables.TemplateColumn(STATUS_LABEL)
|
||||||
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.urls import reverse
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||||
from ipam.models import (
|
from ipam.constants import IP_PROTOCOL_TCP, IP_PROTOCOL_UDP
|
||||||
Aggregate, IPAddress, IP_PROTOCOL_TCP, IP_PROTOCOL_UDP, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF,
|
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
)
|
|
||||||
from users.models import Token
|
from users.models import Token
|
||||||
from utilities.tests import HttpStatusMixin
|
from utilities.tests import HttpStatusMixin
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ urlpatterns = [
|
|||||||
url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
|
url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
|
||||||
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
|
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
|
||||||
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
||||||
|
url(r'^prefixes/(?P<pk>\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
|
||||||
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
|
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
|
||||||
|
|
||||||
# IP addresses
|
# IP addresses
|
||||||
@@ -60,6 +61,7 @@ urlpatterns = [
|
|||||||
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
||||||
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||||
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||||
|
url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
|
||||||
url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
|
url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
|
||||||
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||||
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django_tables2 import RequestConfig
|
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from dcim.models import Device, Interface
|
from dcim.models import Device, Interface
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator
|
||||||
@@ -17,11 +16,8 @@ from utilities.views import (
|
|||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .constants import IPADDRESS_ROLE_ANYCAST
|
from .constants import IPADDRESS_ROLE_ANYCAST, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED
|
||||||
from .models import (
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
|
|
||||||
Service, VLAN, VLANGroup, VRF,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def add_available_prefixes(parent, prefix_list):
|
def add_available_prefixes(parent, prefix_list):
|
||||||
@@ -458,11 +454,6 @@ class PrefixView(View):
|
|||||||
except Aggregate.DoesNotExist:
|
except Aggregate.DoesNotExist:
|
||||||
aggregate = None
|
aggregate = None
|
||||||
|
|
||||||
# Count child IP addresses
|
|
||||||
ipaddress_count = IPAddress.objects.filter(
|
|
||||||
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
|
|
||||||
).count()
|
|
||||||
|
|
||||||
# Parent prefixes table
|
# Parent prefixes table
|
||||||
parent_prefixes = Prefix.objects.filter(
|
parent_prefixes = Prefix.objects.filter(
|
||||||
Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
|
Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
|
||||||
@@ -485,23 +476,38 @@ class PrefixView(View):
|
|||||||
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
|
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
|
||||||
duplicate_prefix_table.exclude = ('vrf',)
|
duplicate_prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
|
return render(request, 'ipam/prefix.html', {
|
||||||
|
'prefix': prefix,
|
||||||
|
'aggregate': aggregate,
|
||||||
|
'parent_prefix_table': parent_prefix_table,
|
||||||
|
'duplicate_prefix_table': duplicate_prefix_table,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class PrefixPrefixesView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
||||||
|
|
||||||
# Child prefixes table
|
# Child prefixes table
|
||||||
child_prefixes = Prefix.objects.filter(
|
child_prefixes = prefix.get_child_prefixes().select_related(
|
||||||
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
|
|
||||||
).select_related(
|
|
||||||
'site', 'vlan', 'role',
|
'site', 'vlan', 'role',
|
||||||
).annotate_depth(limit=0)
|
).annotate_depth(limit=0)
|
||||||
|
|
||||||
|
# Annotate available prefixes
|
||||||
if child_prefixes:
|
if child_prefixes:
|
||||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||||
child_prefix_table = tables.PrefixDetailTable(child_prefixes)
|
|
||||||
|
prefix_table = tables.PrefixDetailTable(child_prefixes)
|
||||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||||
child_prefix_table.columns.show('pk')
|
prefix_table.columns.show('pk')
|
||||||
|
|
||||||
paginate = {
|
paginate = {
|
||||||
'klass': EnhancedPaginator,
|
'klass': EnhancedPaginator,
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
}
|
}
|
||||||
RequestConfig(request, paginate).configure(child_prefix_table)
|
RequestConfig(request, paginate).configure(prefix_table)
|
||||||
|
|
||||||
# Compile permissions list for rendering the object table
|
# Compile permissions list for rendering the object table
|
||||||
permissions = {
|
permissions = {
|
||||||
@@ -510,15 +516,12 @@ class PrefixView(View):
|
|||||||
'delete': request.user.has_perm('ipam.delete_prefix'),
|
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'ipam/prefix.html', {
|
return render(request, 'ipam/prefix_prefixes.html', {
|
||||||
'prefix': prefix,
|
'prefix': prefix,
|
||||||
'aggregate': aggregate,
|
'first_available_prefix': prefix.get_first_available_prefix(),
|
||||||
'ipaddress_count': ipaddress_count,
|
'prefix_table': prefix_table,
|
||||||
'parent_prefix_table': parent_prefix_table,
|
|
||||||
'child_prefix_table': child_prefix_table,
|
|
||||||
'duplicate_prefix_table': duplicate_prefix_table,
|
|
||||||
'permissions': permissions,
|
'permissions': permissions,
|
||||||
'return_url': prefix.get_absolute_url(),
|
'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -529,9 +532,7 @@ class PrefixIPAddressesView(View):
|
|||||||
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
||||||
|
|
||||||
# Find all IPAddresses belonging to this Prefix
|
# Find all IPAddresses belonging to this Prefix
|
||||||
ipaddresses = IPAddress.objects.filter(
|
ipaddresses = prefix.get_child_ips().select_related(
|
||||||
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
|
|
||||||
).select_related(
|
|
||||||
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
|
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
|
||||||
)
|
)
|
||||||
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
|
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
|
||||||
@@ -555,9 +556,10 @@ class PrefixIPAddressesView(View):
|
|||||||
|
|
||||||
return render(request, 'ipam/prefix_ipaddresses.html', {
|
return render(request, 'ipam/prefix_ipaddresses.html', {
|
||||||
'prefix': prefix,
|
'prefix': prefix,
|
||||||
|
'first_available_ip': prefix.get_first_available_ip(),
|
||||||
'ip_table': ip_table,
|
'ip_table': ip_table,
|
||||||
'permissions': permissions,
|
'permissions': permissions,
|
||||||
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
|
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -693,6 +695,51 @@ class IPAddressEditView(IPAddressCreateView):
|
|||||||
permission_required = 'ipam.change_ipaddress'
|
permission_required = 'ipam.change_ipaddress'
|
||||||
|
|
||||||
|
|
||||||
|
class IPAddressAssignView(PermissionRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
Search for IPAddresses to be assigned to an Interface.
|
||||||
|
"""
|
||||||
|
permission_required = 'ipam.change_ipaddress'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
# Redirect user if an interface has not been provided
|
||||||
|
if 'interface' not in request.GET:
|
||||||
|
return redirect('ipam:ipaddress_add')
|
||||||
|
|
||||||
|
return super(IPAddressAssignView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
|
||||||
|
form = forms.IPAddressAssignForm()
|
||||||
|
|
||||||
|
return render(request, 'ipam/ipaddress_assign.html', {
|
||||||
|
'form': form,
|
||||||
|
'return_url': request.GET.get('return_url', ''),
|
||||||
|
})
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
|
||||||
|
form = forms.IPAddressAssignForm(request.POST)
|
||||||
|
table = None
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
queryset = IPAddress.objects.select_related(
|
||||||
|
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
|
||||||
|
).filter(
|
||||||
|
vrf=form.cleaned_data['vrf'],
|
||||||
|
address__net_host=form.cleaned_data['address'],
|
||||||
|
)
|
||||||
|
table = tables.IPAddressAssignTable(queryset)
|
||||||
|
|
||||||
|
return render(request, 'ipam/ipaddress_assign.html', {
|
||||||
|
'form': form,
|
||||||
|
'table': table,
|
||||||
|
'return_url': request.GET.get('return_url', ''),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_ipaddress'
|
permission_required = 'ipam.delete_ipaddress'
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
|
|||||||
def show_form_for_method(self, *args, **kwargs):
|
def show_form_for_method(self, *args, **kwargs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_filter_form(self, data, view, request):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Authentication
|
# Authentication
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django import forms
|
|||||||
|
|
||||||
from utilities.forms import BootstrapMixin
|
from utilities.forms import BootstrapMixin
|
||||||
|
|
||||||
|
|
||||||
OBJ_TYPE_CHOICES = (
|
OBJ_TYPE_CHOICES = (
|
||||||
('', 'All Objects'),
|
('', 'All Objects'),
|
||||||
('Circuits', (
|
('Circuits', (
|
||||||
@@ -39,7 +38,7 @@ OBJ_TYPE_CHOICES = (
|
|||||||
|
|
||||||
class SearchForm(BootstrapMixin, forms.Form):
|
class SearchForm(BootstrapMixin, forms.Form):
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'})
|
label='Search'
|
||||||
)
|
)
|
||||||
obj_type = forms.ChoiceField(
|
obj_type = forms.ChoiceField(
|
||||||
choices=OBJ_TYPE_CHOICES, required=False, label='Type'
|
choices=OBJ_TYPE_CHOICES, required=False, label='Type'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
VERSION = '2.2.4'
|
VERSION = '2.2.9'
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@@ -148,6 +148,7 @@ MIDDLEWARE = (
|
|||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'utilities.middleware.ExceptionHandlingMiddleware',
|
||||||
'utilities.middleware.LoginRequiredMiddleware',
|
'utilities.middleware.LoginRequiredMiddleware',
|
||||||
'utilities.middleware.APIVersionMiddleware',
|
'utilities.middleware.APIVersionMiddleware',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework_swagger.views import get_swagger_view
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.views.static import serve
|
from django.views.static import serve
|
||||||
|
from rest_framework_swagger.views import get_swagger_view
|
||||||
|
|
||||||
from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500
|
from netbox.views import APIRootView, HomeView, SearchView
|
||||||
from users.views import LoginView, LogoutView
|
from users.views import LoginView, LogoutView
|
||||||
|
|
||||||
|
|
||||||
handler500 = handle_500
|
|
||||||
swagger_view = get_swagger_view(title='NetBox API')
|
swagger_view = get_swagger_view(title='NetBox API')
|
||||||
|
|
||||||
_patterns = [
|
_patterns = [
|
||||||
@@ -48,9 +45,6 @@ _patterns = [
|
|||||||
# Serving static media in Django to pipe it through LoginRequiredMiddleware
|
# Serving static media in Django to pipe it through LoginRequiredMiddleware
|
||||||
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||||
|
|
||||||
# Error testing
|
|
||||||
url(r'^500/$', trigger_500),
|
|
||||||
|
|
||||||
# Admin
|
# Admin
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import sys
|
|
||||||
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.reverse import reverse
|
|
||||||
|
|
||||||
|
from django.db.models import Count
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from circuits.filters import CircuitFilter, ProviderFilter
|
from circuits.filters import CircuitFilter, ProviderFilter
|
||||||
from circuits.models import Circuit, Provider
|
from circuits.models import Circuit, Provider
|
||||||
@@ -15,7 +15,7 @@ from circuits.tables import CircuitTable, ProviderTable
|
|||||||
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
|
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
|
||||||
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
|
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
|
||||||
from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable
|
from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable
|
||||||
from extras.models import TopologyMap, UserAction
|
from extras.models import ReportResult, TopologyMap, UserAction
|
||||||
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||||
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
||||||
@@ -30,7 +30,6 @@ from virtualization.models import Cluster, VirtualMachine
|
|||||||
from virtualization.tables import ClusterTable, VirtualMachineDetailTable
|
from virtualization.tables import ClusterTable, VirtualMachineDetailTable
|
||||||
from .forms import SearchForm
|
from .forms import SearchForm
|
||||||
|
|
||||||
|
|
||||||
SEARCH_MAX_RESULTS = 15
|
SEARCH_MAX_RESULTS = 15
|
||||||
SEARCH_TYPES = OrderedDict((
|
SEARCH_TYPES = OrderedDict((
|
||||||
# Circuits
|
# Circuits
|
||||||
@@ -60,7 +59,7 @@ SEARCH_TYPES = OrderedDict((
|
|||||||
'url': 'dcim:rack_list',
|
'url': 'dcim:rack_list',
|
||||||
}),
|
}),
|
||||||
('devicetype', {
|
('devicetype', {
|
||||||
'queryset': DeviceType.objects.select_related('manufacturer'),
|
'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
|
||||||
'filter': DeviceTypeFilter,
|
'filter': DeviceTypeFilter,
|
||||||
'table': DeviceTypeTable,
|
'table': DeviceTypeTable,
|
||||||
'url': 'dcim:devicetype_list',
|
'url': 'dcim:devicetype_list',
|
||||||
@@ -178,6 +177,7 @@ class HomeView(View):
|
|||||||
'search_form': SearchForm(),
|
'search_form': SearchForm(),
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
|
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
|
||||||
|
'report_results': ReportResult.objects.order_by('-created')[:10],
|
||||||
'recent_activity': UserAction.objects.select_related('user')[:50]
|
'recent_activity': UserAction.objects.select_related('user')[:50]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -247,23 +247,3 @@ class APIRootView(APIView):
|
|||||||
('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
|
('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
|
||||||
('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
|
('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
|
||||||
def handle_500(request):
|
|
||||||
"""
|
|
||||||
Custom server error handler
|
|
||||||
"""
|
|
||||||
type_, error, traceback = sys.exc_info()
|
|
||||||
return render(request, '500.html', {
|
|
||||||
'exception': str(type_),
|
|
||||||
'error': error,
|
|
||||||
}, status=500)
|
|
||||||
|
|
||||||
|
|
||||||
def trigger_500(request):
|
|
||||||
"""
|
|
||||||
Hot-wired method of triggering a server error to test reporting
|
|
||||||
"""
|
|
||||||
raise Exception(
|
|
||||||
"Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are."
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,12 +1,3 @@
|
|||||||
"""
|
|
||||||
WSGI config for do_ipam project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|||||||
@@ -35,8 +35,22 @@ footer p {
|
|||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collapse the nav menu on displays less than 1200px wide */
|
/* Hide the username in the navigation menu on displays less than 1400px wide */
|
||||||
|
@media (max-width: 1399px) {
|
||||||
|
#navbar_user {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the search bar in the navigation menu on displays less than 1200px wide */
|
||||||
@media (max-width: 1199px) {
|
@media (max-width: 1199px) {
|
||||||
|
#navbar_search {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapse the nav menu on displays less than 960px wide */
|
||||||
|
@media (max-width: 959px) {
|
||||||
.navbar-header {
|
.navbar-header {
|
||||||
float: none;
|
float: none;
|
||||||
}
|
}
|
||||||
@@ -72,12 +86,8 @@ footer p {
|
|||||||
.collapse.in {
|
.collapse.in {
|
||||||
display:block !important;
|
display:block !important;
|
||||||
}
|
}
|
||||||
}
|
#navbar_user {
|
||||||
|
display: inline;
|
||||||
/* Hide the nav search bar on displays less than 1600px wide */
|
|
||||||
@media (max-width: 1599px) {
|
|
||||||
#navbar_search {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,6 +405,9 @@ table.reports td.stats label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
|
.text-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.banner-bottom {
|
.banner-bottom {
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
from django.http import HttpResponseBadRequest
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet, ViewSet
|
from rest_framework.viewsets import ModelViewSet, ViewSet
|
||||||
|
|
||||||
from django.http import HttpResponseBadRequest
|
|
||||||
|
|
||||||
from secrets import filters
|
from secrets import filters
|
||||||
from secrets.exceptions import InvalidKey
|
from secrets.exceptions import InvalidKey
|
||||||
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||||
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
|
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
ERR_USERKEY_MISSING = "No UserKey found for the current user."
|
ERR_USERKEY_MISSING = "No UserKey found for the current user."
|
||||||
ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption."
|
ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption."
|
||||||
ERR_PRIVKEY_MISSING = "Private key was not provided."
|
ERR_PRIVKEY_MISSING = "Private key was not provided."
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from .models import Secret, SecretRole
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.filters import NumericInFilter
|
from utilities.filters import NumericInFilter
|
||||||
|
from .models import Secret, SecretRole
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleFilter(django_filters.FilterSet):
|
class SecretRoleFilter(django_filters.FilterSet):
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from Crypto.Cipher import PKCS1_OAEP
|
from Crypto.Cipher import PKCS1_OAEP
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from Crypto.Cipher import AES, PKCS1_OAEP
|
from Crypto.Cipher import AES, PKCS1_OAEP
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from Crypto.Util import strxor
|
from Crypto.Util import strxor
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password, check_password
|
from django.contrib.auth.hashers import make_password, check_password
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
@@ -286,6 +286,9 @@ class Secret(CreatedUpdatedModel):
|
|||||||
super(Secret, self).__init__(*args, **kwargs)
|
super(Secret, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if self.role and self.device and self.name:
|
||||||
|
return '{} for {} ({})'.format(self.role, self.device, self.name)
|
||||||
|
# Return role and device if no name is set
|
||||||
if self.role and self.device:
|
if self.role and self.device:
|
||||||
return '{} for {}'.format(self.role, self.device)
|
return '{} for {}'.format(self.role, self.device)
|
||||||
return 'Secret'
|
return 'Secret'
|
||||||
@@ -300,6 +303,7 @@ class Secret(CreatedUpdatedModel):
|
|||||||
|LL|MySecret|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|
|LL|MySecret|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|
||||||
+--+--------+-------------------------------------------+
|
+--+--------+-------------------------------------------+
|
||||||
"""
|
"""
|
||||||
|
s = s.encode('utf8')
|
||||||
if len(s) > 65535:
|
if len(s) > 65535:
|
||||||
raise ValueError("Maximum plaintext size is 65535 bytes.")
|
raise ValueError("Maximum plaintext size is 65535 bytes.")
|
||||||
# Minimum ciphertext size is 64 bytes to conceal the length of short secrets.
|
# Minimum ciphertext size is 64 bytes to conceal the length of short secrets.
|
||||||
@@ -312,7 +316,7 @@ class Secret(CreatedUpdatedModel):
|
|||||||
return (
|
return (
|
||||||
chr(len(s) >> 8).encode() +
|
chr(len(s) >> 8).encode() +
|
||||||
chr(len(s) % 256).encode() +
|
chr(len(s) % 256).encode() +
|
||||||
s.encode() +
|
s +
|
||||||
os.urandom(pad_length)
|
os.urandom(pad_length)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -321,11 +325,11 @@ class Secret(CreatedUpdatedModel):
|
|||||||
Consume the first two bytes of s as a plaintext length indicator and return only that many bytes as the
|
Consume the first two bytes of s as a plaintext length indicator and return only that many bytes as the
|
||||||
plaintext.
|
plaintext.
|
||||||
"""
|
"""
|
||||||
if isinstance(s[0], int):
|
if isinstance(s[0], str):
|
||||||
plaintext_length = (s[0] << 8) + s[1]
|
|
||||||
elif isinstance(s[0], str):
|
|
||||||
plaintext_length = (ord(s[0]) << 8) + ord(s[1])
|
plaintext_length = (ord(s[0]) << 8) + ord(s[1])
|
||||||
return s[2:plaintext_length + 2].decode()
|
else:
|
||||||
|
plaintext_length = (s[0] << 8) + s[1]
|
||||||
|
return s[2:plaintext_length + 2].decode('utf8')
|
||||||
|
|
||||||
def encrypt(self, secret_key):
|
def encrypt(self, secret_key):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ from __future__ import unicode_literals
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from utilities.tables import BaseTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
|
|
||||||
from .models import SecretRole, Secret
|
from .models import SecretRole, Secret
|
||||||
|
|
||||||
|
|
||||||
SECRETROLE_ACTIONS = """
|
SECRETROLE_ACTIONS = """
|
||||||
{% if perms.secrets.change_secretrole %}
|
{% if perms.secrets.change_secretrole %}
|
||||||
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import base64
|
|
||||||
|
|
||||||
from rest_framework import status
|
import base64
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||||
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||||
from users.models import Token
|
from users.models import Token
|
||||||
from utilities.tests import HttpStatusMixin
|
from utilities.tests import HttpStatusMixin
|
||||||
|
|
||||||
|
|
||||||
# Dummy RSA key pair for testing use only
|
# Dummy RSA key pair for testing use only
|
||||||
PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
|
PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z
|
MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
|
|
||||||
from secrets.hashers import SecretValidationHasher
|
from secrets.hashers import SecretValidationHasher
|
||||||
|
from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
|
||||||
|
|
||||||
|
|
||||||
class UserKeyTestCase(TestCase):
|
class UserKeyTestCase(TestCase):
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.conf.urls import url
|
|||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
app_name = 'secrets'
|
app_name = 'secrets'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -165,7 +166,7 @@ def secret_edit(request, pk):
|
|||||||
# Create and encrypt the new Secret
|
# Create and encrypt the new Secret
|
||||||
if master_key is not None:
|
if master_key is not None:
|
||||||
secret = form.save(commit=False)
|
secret = form.save(commit=False)
|
||||||
secret.plaintext = str(form.cleaned_data['plaintext'])
|
secret.plaintext = form.cleaned_data['plaintext']
|
||||||
secret.encrypt(master_key)
|
secret.encrypt(master_key)
|
||||||
secret.save()
|
secret.save()
|
||||||
messages.success(request, "Modified secret {}.".format(secret))
|
messages.success(request, "Modified secret {}.".format(secret))
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 col-md-offset-4">
|
<div class="col-md-6 col-md-offset-3">
|
||||||
<div class="panel panel-danger" style="margin-top: 200px">
|
<div class="panel panel-danger" style="margin-top: 200px">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>
|
<strong>
|
||||||
@@ -21,13 +21,20 @@
|
|||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p>There was a problem with your request. This error has been logged and administrative staff have
|
{% block message %}
|
||||||
been notified. Please return to the home page and try again.</p>
|
<p>
|
||||||
<p>If you are responsible for this installation, please consider
|
There was a problem with your request. Please contact an administrator.
|
||||||
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>. Additional
|
</p>
|
||||||
information is provided below:</p>
|
{% endblock %}
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
The complete exception is provided below:
|
||||||
|
</p>
|
||||||
<pre><strong>{{ exception }}</strong><br />
|
<pre><strong>{{ exception }}</strong><br />
|
||||||
{{ error }}</pre>
|
{{ error }}</pre>
|
||||||
|
<p>
|
||||||
|
If further assistance is required, please post to the <a href="https://groups.google.com/forum/#!forum/netbox-discuss">NetBox mailing list</a>.
|
||||||
|
</p>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
|
<a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if circuit.tenant %}
|
{% if circuit.tenant %}
|
||||||
{% if circuit.tenant.group %}
|
{% if circuit.tenant.group %}
|
||||||
<a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group.name }}</a>
|
<a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group }}</a>
|
||||||
<i class="fa fa-angle-right"></i>
|
<i class="fa fa-angle-right"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
|
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a circuit type
|
Add a circuit type
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'circuits:circuittype_import' %}" class="btn btn-info">
|
||||||
|
<span class="fa fa-download" aria-hidden="true"></span>
|
||||||
|
Import circuit types
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Circuit Types{% endblock %}</h1>
|
<h1>{% block title %}Circuit Types{% endblock %}</h1>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{% 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">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Device</strong>
|
<strong>Device</strong>
|
||||||
@@ -28,10 +28,10 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if device.rack %}
|
{% if device.rack %}
|
||||||
{% if device.rack.group %}
|
{% if device.rack.group %}
|
||||||
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group.name }}</a>
|
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group }}</a>
|
||||||
<i class="fa fa-angle-right"></i>
|
<i class="fa fa-angle-right"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}
|
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if device.parent_bay %}
|
{% if device.parent_bay %}
|
||||||
{% with device.parent_bay.device as parent %}
|
{% with device.parent_bay.device as parent %}
|
||||||
<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay.name }}
|
<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay }}
|
||||||
{% if parent.position %}
|
{% if parent.position %}
|
||||||
(U{{ parent.position }} / {{ parent.get_face_display }})
|
(U{{ parent.position }} / {{ parent.get_face_display }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if device.tenant %}
|
{% if device.tenant %}
|
||||||
{% if device.tenant.group %}
|
{% if device.tenant.group %}
|
||||||
<a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group.name }}</a>
|
<a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group }}</a>
|
||||||
<i class="fa fa-angle-right"></i>
|
<i class="fa fa-angle-right"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
|
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
|
||||||
@@ -172,6 +172,69 @@
|
|||||||
{% with device.get_custom_fields as custom_fields %}
|
{% with device.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Comments</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% if device.comments %}
|
||||||
|
{{ device.comments|gfm }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Console / Power</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover panel-body component-list">
|
||||||
|
{% for cp in console_ports %}
|
||||||
|
{% include 'dcim/inc/consoleport.html' %}
|
||||||
|
{% empty %}
|
||||||
|
{% if device.device_type.console_port_templates.exists %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="alert-warning">
|
||||||
|
<i class="fa fa-fw fa-warning"></i> No console ports defined
|
||||||
|
{% if perms.dcim.add_consoleport %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for pp in power_ports %}
|
||||||
|
{% include 'dcim/inc/powerport.html' %}
|
||||||
|
{% empty %}
|
||||||
|
{% if device.device_type.power_port_templates.exists %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="alert-warning">
|
||||||
|
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
||||||
|
{% if perms.dcim.add_powerport %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
{% if perms.dcim.add_consoleport %}
|
||||||
|
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_powerport %}
|
||||||
|
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
@@ -224,67 +287,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Console / Power</strong>
|
|
||||||
</div>
|
|
||||||
<table class="table table-hover panel-body component-list">
|
|
||||||
{% for cp in console_ports %}
|
|
||||||
{% include 'dcim/inc/consoleport.html' %}
|
|
||||||
{% empty %}
|
|
||||||
{% if device.device_type.console_port_templates.exists %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="alert-warning">
|
|
||||||
<i class="fa fa-fw fa-warning"></i> No console ports defined
|
|
||||||
{% if perms.dcim.add_consoleport %}
|
|
||||||
<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>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% for pp in power_ports %}
|
|
||||||
{% include 'dcim/inc/powerport.html' %}
|
|
||||||
{% empty %}
|
|
||||||
{% if device.device_type.power_port_templates.exists %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="alert-warning">
|
|
||||||
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
|
||||||
{% if perms.dcim.add_powerport %}
|
|
||||||
<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>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
|
||||||
<div class="panel-footer text-right">
|
|
||||||
{% if perms.dcim.add_consoleport %}
|
|
||||||
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.add_powerport %}
|
|
||||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Comments</strong>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{% if device.comments %}
|
|
||||||
{{ device.comments|gfm }}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">None</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Images</strong>
|
<strong>Images</strong>
|
||||||
@@ -326,7 +328,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-7">
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
{% if device_bays or device.device_type.is_parent_device %}
|
{% if device_bays or device.device_type.is_parent_device %}
|
||||||
{% if perms.dcim.delete_devicebay %}
|
{% if perms.dcim.delete_devicebay %}
|
||||||
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
||||||
@@ -350,7 +354,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body component-list">
|
<table class="table table-hover panel-body component-list">
|
||||||
{% for devicebay in device_bays %}
|
{% for devicebay in device_bays %}
|
||||||
{% include 'dcim/inc/devicebay.html' with selectable=True %}
|
{% include 'dcim/inc/devicebay.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">No device bays defined</td>
|
<td colspan="4">No device bays defined</td>
|
||||||
@@ -405,11 +409,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table id="interfaces_table" class="table table-hover panel-body component-list">
|
<table id="interfaces_table" class="table table-hover panel-body component-list">
|
||||||
|
<tr class="table-headings">
|
||||||
|
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||||
|
<th></th>
|
||||||
|
{% endif %}
|
||||||
|
<th>Name</th>
|
||||||
|
<th>LAG</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>MTU</th>
|
||||||
|
<th>MAC Address</th>
|
||||||
|
<th colspan="2">Connection</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
{% for iface in interfaces %}
|
{% for iface in interfaces %}
|
||||||
{% include 'dcim/inc/interface.html' with selectable=True %}
|
{% include 'dcim/inc/interface.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">No interfaces defined</td>
|
<td colspan="8">No interfaces defined</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
@@ -467,8 +483,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body component-list">
|
<table class="table table-hover panel-body component-list">
|
||||||
|
<tr class="table-headings">
|
||||||
|
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||||
|
<th></th>
|
||||||
|
{% endif %}
|
||||||
|
<th>Name</th>
|
||||||
|
<th colspan="2">Connection</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
{% for csp in cs_ports %}
|
{% for csp in cs_ports %}
|
||||||
{% include 'dcim/inc/consoleserverport.html' with selectable=True %}
|
{% include 'dcim/inc/consoleserverport.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">No console server ports defined</td>
|
<td colspan="4">No console server ports defined</td>
|
||||||
@@ -511,7 +535,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Power Outlets</strong>
|
<strong>Power Outlets</strong>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.dcim.change_poweroutlet and cs_ports|length > 1 %}
|
{% if perms.dcim.change_poweroutlet and power_outlets|length > 1 %}
|
||||||
<button class="btn btn-default btn-xs toggle">
|
<button class="btn btn-default btn-xs toggle">
|
||||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
</button>
|
</button>
|
||||||
@@ -524,12 +548,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body component-list">
|
<table class="table table-hover panel-body component-list">
|
||||||
|
<tr class="table-headings">
|
||||||
|
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||||
|
<th></th>
|
||||||
|
{% endif %}
|
||||||
|
<th>Name</th>
|
||||||
|
<th colspan="2">Connection</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
{% for po in power_outlets %}
|
{% for po in power_outlets %}
|
||||||
{% include 'dcim/inc/poweroutlet.html' with selectable=True %}
|
{% include 'dcim/inc/poweroutlet.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">No power outlets defined</td>
|
<td colspan="4">No power outlets defined</td>
|
||||||
</tr>
|
</tr> text-nowrap
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
|
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||||
|
|||||||
@@ -64,13 +64,14 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
{% if perms.dcim.add_inventoryitem %}
|
{% if perms.dcim.add_inventoryitem %}
|
||||||
<a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-success">
|
<div class="panel-footer text-right">
|
||||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
<a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
Add Inventory Item
|
<span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -58,9 +58,10 @@ $(document).ready(function() {
|
|||||||
// Glean configured hostnames/interfaces from the DOM
|
// Glean configured hostnames/interfaces from the DOM
|
||||||
var configured_device = row.children('td.configured_device').attr('data');
|
var configured_device = row.children('td.configured_device').attr('data');
|
||||||
var configured_interface = row.children('td.configured_interface').attr('data');
|
var configured_interface = row.children('td.configured_interface').attr('data');
|
||||||
|
var configured_interface_short = null;
|
||||||
if (configured_interface) {
|
if (configured_interface) {
|
||||||
// Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1).
|
// Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1).
|
||||||
configured_interface = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2");
|
configured_interface_short = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up hostnames/interfaces learned via LLDP
|
// Clean up hostnames/interfaces learned via LLDP
|
||||||
@@ -76,6 +77,8 @@ $(document).ready(function() {
|
|||||||
row.addClass('info');
|
row.addClass('info');
|
||||||
} else if (configured_device == lldp_device && configured_interface == lldp_interface) {
|
} else if (configured_device == lldp_device && configured_interface == lldp_interface) {
|
||||||
row.addClass('success');
|
row.addClass('success');
|
||||||
|
} else if (configured_device == lldp_device && configured_interface_short == lldp_interface) {
|
||||||
|
row.addClass('success');
|
||||||
} else {
|
} else {
|
||||||
row.addClass('danger');
|
row.addClass('danger');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label required">Bay</label>
|
<label class="col-md-3 control-label required">Bay</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<p class="form-control-static">{{ device_bay.name }}</p>
|
<p class="form-control-static">{{ device_bay }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a device role
|
Add a device role
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'dcim:devicerole_import' %}" class="btn btn-info">
|
||||||
|
<span class="fa fa-download" aria-hidden="true"></span>
|
||||||
|
Import device roles
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Device Roles{% endblock %}</h1>
|
<h1>{% block title %}Device Roles{% endblock %}</h1>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
|
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
|
||||||
</td>
|
</td>
|
||||||
{% if cp.cs_port %}
|
{% if cp.cs_port %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
|
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ cp.cs_port.name }}
|
{{ cp.cs_port }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}">
|
<tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}">
|
||||||
{% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
|
{% if 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>
|
||||||
@@ -7,20 +7,19 @@
|
|||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
|
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
|
||||||
{% if csp.connected_console %}
|
{% if csp.connected_console %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
|
<a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ csp.connected_console.name }}
|
{{ csp.connected_console }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<span class="text-muted">Not connected</span>
|
<span class="text-muted">Not connected</span>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td colspan="2" class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_consoleserverport %}
|
{% if perms.dcim.change_consoleserverport %}
|
||||||
{% if csp.connected_console %}
|
{% if csp.connected_console %}
|
||||||
{% if csp.connected_console.connection_status %}
|
{% if csp.connected_console.connection_status %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if device.parent_bay %}
|
{% if device.parent_bay %}
|
||||||
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
|
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
|
||||||
<li>{{ device.parent_bay.name }}</li>
|
<li>{{ device.parent_bay }}</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>{{ device }}</li>
|
<li>{{ device }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<tr class="devicebay">
|
<tr class="devicebay">
|
||||||
{% if selectable and perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
|
{% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
|
||||||
<td class="pk">
|
<td class="pk">
|
||||||
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
|
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
|
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay }}
|
||||||
</td>
|
</td>
|
||||||
{% if devicebay.installed_device %}
|
{% if devicebay.installed_device %}
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}">
|
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
|
||||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
|
||||||
|
{# Checkbox #}
|
||||||
|
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||||
<td class="pk">
|
<td class="pk">
|
||||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Icon and name #}
|
||||||
|
<td>
|
||||||
|
<span title="{{ iface.get_form_factor_display }}">
|
||||||
|
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}align-justify{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
|
||||||
|
{{ iface }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# LAG #}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_virtual %}square{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
|
|
||||||
<span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
|
||||||
{% if iface.lag %}
|
{% if iface.lag %}
|
||||||
<span class="label label-primary">{{ iface.lag.name }}</span>
|
<a href="#iface_{{ iface.lag }}" class="label label-default">{{ iface.lag }}</a>
|
||||||
{% endif %}
|
|
||||||
{% if iface.description %}
|
|
||||||
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Description #}
|
||||||
|
<td>{{ iface.description }}</td>
|
||||||
|
|
||||||
|
{# MTU #}
|
||||||
<td>{{ iface.mtu|default:"" }}</td>
|
<td>{{ iface.mtu|default:"" }}</td>
|
||||||
|
|
||||||
|
{# MAC address #}
|
||||||
<td>{{ iface.mac_address|default:"" }}</td>
|
<td>{{ iface.mac_address|default:"" }}</td>
|
||||||
|
|
||||||
|
{# Connection or type #}
|
||||||
{% if iface.is_lag %}
|
{% if iface.is_lag %}
|
||||||
<td colspan="2" class="text-muted">
|
<td colspan="2" class="text-muted">
|
||||||
LAG interface<br />
|
LAG interface<br />
|
||||||
@@ -55,7 +71,9 @@
|
|||||||
<span class="text-muted">Not connected</span>
|
<span class="text-muted">Not connected</span>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td colspan="2" class="text-right">
|
|
||||||
|
{# Buttons #}
|
||||||
|
<td class="text-right text-nowrap">
|
||||||
{% if show_graphs %}
|
{% if show_graphs %}
|
||||||
{% if iface.circuit_termination or iface.connection %}
|
{% if iface.circuit_termination or iface.connection %}
|
||||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
|
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
|
||||||
@@ -96,7 +114,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:interface_edit' pk=iface.pk %}" class="btn btn-info btn-xs" title="Edit interface">
|
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}" class="btn btn-info btn-xs" title="Edit interface">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -106,47 +124,53 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
|
<a href="{% if iface.device_id %}{% url 'dcim:interface_delete' pk=iface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=iface.pk %}{% endif %}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% with iface.ip_addresses.all as ipaddresses %}
|
{% for ip in iface.ip_addresses.all %}
|
||||||
{% if ipaddresses %}
|
|
||||||
<tr class="ipaddress">
|
<tr class="ipaddress">
|
||||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
|
||||||
|
{# Placeholder #}
|
||||||
|
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||||
<td></td>
|
<td></td>
|
||||||
<td colspan="6" class="subtable">
|
|
||||||
{% else %}
|
|
||||||
<td colspan="7" class="subtable">
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<table class="table table-hover">
|
|
||||||
{% for ip in ipaddresses %}
|
{# IP address #}
|
||||||
<tr>
|
<td colspan="2">
|
||||||
<td>
|
|
||||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||||
{% if ip.description %}
|
|
||||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Primary, status, role #}
|
||||||
<td>
|
<td>
|
||||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||||
<span class="label label-success">Primary</span>
|
<span class="label label-success">Primary</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||||
<td>
|
{% if ip.role %}
|
||||||
{% if ip.vrf %}
|
<span class="label label-{{ ip.get_role_class }}">{{ ip.get_role_display }}</span>
|
||||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">Global table</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
{# VRF #}
|
||||||
|
<td colspan="2">
|
||||||
|
{% if ip.vrf %}
|
||||||
|
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}" title="{{ ip.vrf.rd }}">{{ ip.vrf.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Global</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
|
||||||
|
{# Description #}
|
||||||
|
<td colspan="2">
|
||||||
|
{{ ip.description }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Buttons #}
|
||||||
|
<td class="text-right text-nowrap">
|
||||||
{% if perms.ipam.change_ipaddress %}
|
{% if perms.ipam.change_ipaddress %}
|
||||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||||
@@ -160,8 +184,3 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td>
|
<td style="padding-left: {{ indent|add:5 }}px">{{ item }}</td>
|
||||||
<td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
|
<td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
|
||||||
<td>{{ item.manufacturer|default:"" }}</td>
|
<td>{{ item.manufacturer|default:"" }}</td>
|
||||||
<td>{{ item.part_id }}</td>
|
<td>{{ item.part_id }}</td>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_inventoryitem %}
|
{% if perms.dcim.delete_inventoryitem %}
|
||||||
<a href="{% url 'dcim:inventoryitem_delete' pk=item.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>
|
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
<tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}">
|
<tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}">
|
||||||
{% if selectable and perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
|
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||||
<td class="pk">
|
<td class="pk">
|
||||||
<input name="pk" type="checkbox" value="{{ po.pk }}" />
|
<input name="pk" type="checkbox" value="{{ po.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-bolt"></i> {{ po.name }}
|
<i class="fa fa-fw fa-bolt"></i> {{ po }}
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
|
||||||
{% if po.connected_port %}
|
{% if po.connected_port %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
|
<a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ po.connected_port.name }}
|
{{ po.connected_port }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<span class="text-muted">Not connected</span>
|
<span class="text-muted">Not connected</span>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td colspan="2" class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_poweroutlet %}
|
{% if perms.dcim.change_poweroutlet %}
|
||||||
{% if po.connected_port %}
|
{% if po.connected_port %}
|
||||||
{% if po.connected_port.connection_status %}
|
{% if po.connected_port.connection_status %}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
|
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
|
<i class="fa fa-fw fa-bolt"></i> {{ pp }}
|
||||||
</td>
|
</td>
|
||||||
{% if pp.power_outlet %}
|
{% if pp.power_outlet %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
|
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ pp.power_outlet.name }}
|
{{ pp.power_outlet }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
|
|||||||
5
netbox/templates/dcim/inventoryitem_bulk_delete.html
Normal file
5
netbox/templates/dcim/inventoryitem_bulk_delete.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'utilities/obj_bulk_delete.html' %}
|
||||||
|
|
||||||
|
{% block message_extra %}
|
||||||
|
<p class="text-center text-danger"><i class="fa fa-warning"></i> This will also delete all child inventory items of those listed.</p>
|
||||||
|
{% endblock %}
|
||||||
23
netbox/templates/dcim/inventoryitem_list.html
Normal file
23
netbox/templates/dcim/inventoryitem_list.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right">
|
||||||
|
{% if perms.dcim.add_devicetype %}
|
||||||
|
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-info">
|
||||||
|
<span class="fa fa-download" aria-hidden="true"></span>
|
||||||
|
Import inventory items
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'inc/export_button.html' with obj_type='inventory items' %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Inventory Items{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -8,6 +8,10 @@
|
|||||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a platform
|
Add a platform
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'dcim:platform_import' %}" class="btn btn-info">
|
||||||
|
<span class="fa fa-download" aria-hidden="true"></span>
|
||||||
|
Import platforms
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Platforms{% endblock %}</h1>
|
<h1>{% block title %}Platforms{% endblock %}</h1>
|
||||||
|
|||||||
@@ -24,32 +24,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if prev_rack %}
|
<a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
|
||||||
<a href="{% url 'dcim:rack' pk=prev_rack.pk %}" class="btn btn-primary">
|
<span class="fa fa-chevron-left" aria-hidden="true"></span> Previous Rack
|
||||||
<span class="fa fa-chevron-left" aria-hidden="true"></span>
|
|
||||||
Previous Rack
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
<a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
|
||||||
{% if next_rack %}
|
<span class="fa fa-chevron-right" aria-hidden="true"></span> Next Rack
|
||||||
<a href="{% url 'dcim:rack' pk=next_rack.pk %}" class="btn btn-primary">
|
|
||||||
<span class="fa fa-chevron-right" aria-hidden="true"></span>
|
|
||||||
Next Rack
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
|
||||||
{% if perms.dcim.change_rack %}
|
{% if perms.dcim.change_rack %}
|
||||||
<a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
|
<a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
|
||||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this rack
|
||||||
Edit this rack
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_rack %}
|
{% if perms.dcim.delete_rack %}
|
||||||
<a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger">
|
<a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger">
|
||||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
<span class="fa fa-trash" aria-hidden="true"></span> Delete this rack
|
||||||
Delete this rack
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Rack {{ rack.name }}{% endblock %}</h1>
|
<h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
|
||||||
{% include 'inc/created_updated.html' with obj=rack %}
|
{% include 'inc/created_updated.html' with obj=rack %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -72,7 +64,7 @@
|
|||||||
<td>Group</td>
|
<td>Group</td>
|
||||||
<td>
|
<td>
|
||||||
{% if rack.group %}
|
{% if rack.group %}
|
||||||
<a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}&group={{ rack.group.slug }}">{{ rack.group.name }}</a>
|
<a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}&group={{ rack.group.slug }}">{{ rack.group }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -93,7 +85,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if rack.tenant %}
|
{% if rack.tenant %}
|
||||||
{% if rack.tenant.group %}
|
{% if rack.tenant.group %}
|
||||||
<a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group.name }}</a>
|
<a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group }}</a>
|
||||||
<i class="fa fa-angle-right"></i>
|
<i class="fa fa-angle-right"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
|
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
|
||||||
@@ -173,7 +165,7 @@
|
|||||||
{% for device in nonracked_devices %}
|
{% for device in nonracked_devices %}
|
||||||
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device.name }}</a>
|
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ device.device_role }}</td>
|
<td>{{ device.device_role }}</td>
|
||||||
<td>{{ device.device_type.full_name }}</td>
|
<td>{{ device.device_type.full_name }}</td>
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a rack role
|
Add a rack role
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'dcim:rackrole_import' %}" class="btn btn-info">
|
||||||
|
<span class="fa fa-download" aria-hidden="true"></span>
|
||||||
|
Import rack roles
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Rack Roles{% endblock %}</h1>
|
<h1>{% block title %}Rack Roles{% endblock %}</h1>
|
||||||
|
|||||||
@@ -17,8 +17,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Regions{% endblock %}</h1>
|
<h1>{% block title %}Regions{% endblock %}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-9">
|
||||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
|
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if site.tenant %}
|
{% if site.tenant %}
|
||||||
{% if site.tenant.group %}
|
{% if site.tenant.group %}
|
||||||
<a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group.name }}</a>
|
<a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group }}</a>
|
||||||
<i class="fa fa-angle-right"></i>
|
<i class="fa fa-angle-right"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
|
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
|
||||||
@@ -211,6 +211,10 @@
|
|||||||
<h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
|
<h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
|
||||||
<p>Circuits</p>
|
<p>Circuits</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4 text-center">
|
||||||
|
<h2><a href="{% url 'virtualization:virtualmachine_list' %}?site={{ site.slug }}" class="btn {% if stats.vm_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vm_count }}</a></h2>
|
||||||
|
<p>Virtual Machines</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -221,7 +225,7 @@
|
|||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for rg in rack_groups %}
|
{% for rg in rack_groups %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg.name }}</a></td>
|
<td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
|
||||||
<td>{{ rg.rack_count }}</td>
|
<td>{{ rg.rack_count }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
18
netbox/templates/exceptions/import_error.html
Normal file
18
netbox/templates/exceptions/import_error.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends '500.html' %}
|
||||||
|
|
||||||
|
{% block message %}
|
||||||
|
<p>
|
||||||
|
A module import error occurred during this request. Common causes include the following:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-warning"></i> <strong>Missing required packages</strong> - This installation of NetBox might be missing one or more required
|
||||||
|
Python packages. These packages are listed in <code>requirements.txt</code> and are normally installed as part
|
||||||
|
of the installation or upgrade process. To verify installed packages, run <code>pip freeze</code> from the
|
||||||
|
console and compare the output to the list of required packages.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-warning"></i> <strong>WSGI service not restarted after upgrade</strong> - If this installation has recently been upgraded,
|
||||||
|
check that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This ensures that the new code is
|
||||||
|
running.
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
12
netbox/templates/exceptions/permission_error.html
Normal file
12
netbox/templates/exceptions/permission_error.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends '500.html' %}
|
||||||
|
|
||||||
|
{% block message %}
|
||||||
|
<p>
|
||||||
|
A file permission error was detected while processing this request. Common causes include the following:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-warning"></i> <strong>Insufficient write permission to the media root</strong> - The configured
|
||||||
|
media root is <code>{{ settings.MEDIA_ROOT }}</code>. Ensure that the user NetBox runs as has access to write
|
||||||
|
files to all locations within this path.
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
17
netbox/templates/exceptions/programming_error.html
Normal file
17
netbox/templates/exceptions/programming_error.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{% extends '500.html' %}
|
||||||
|
|
||||||
|
{% block message %}
|
||||||
|
<p>
|
||||||
|
A database programming error was detected while processing this request. Common causes include the following:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-warning"></i> <strong>Database migrations missing</strong> - When upgrading to a new NetBox release, the upgrade script must
|
||||||
|
be run to apply any new database migrations. You can run migrations manually by executing
|
||||||
|
<code>python3 manage.py migrate</code> from the command line.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-warning"></i> <strong>Unsupported PostgreSQL version</strong> - Ensure that PostgreSQL version 9.4 or higher is in use. You
|
||||||
|
can check this by connecting to the database using NetBox's credentials and issuing a query for
|
||||||
|
<code>SELECT VERSION()</code>.
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{% if report.result.failed %}
|
{% if result.failed %}
|
||||||
<label class="label label-danger">Failed</label>
|
<label class="label label-danger">Failed</label>
|
||||||
{% elif report.result %}
|
{% elif result %}
|
||||||
<label class="label label-success">Passed</label>
|
<label class="label label-success">Passed</label>
|
||||||
{% else %}
|
{% else %}
|
||||||
<label class="label label-default">N/A</label>
|
<label class="label label-default">N/A</label>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h1>{{ report.name }}{% include 'extras/inc/report_label.html' %}</h1>
|
<h1>{{ report.name }}{% include 'extras/inc/report_label.html' with result=report.result %}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% if report.description %}
|
{% if report.description %}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<a href="{% url 'extras:report' name=report.full_name %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a>
|
<a href="{% url 'extras:report' name=report.full_name %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% include 'extras/inc/report_label.html' %}
|
{% include 'extras/inc/report_label.html' with result=report.result %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ report.description|default:"" }}</td>
|
<td>{{ report.description|default:"" }}</td>
|
||||||
{% if report.result %}
|
{% if report.result %}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user