mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-09 05:12:18 -06:00
Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dcb2de28f | ||
|
|
cbd155406b | ||
|
|
70cc368225 | ||
|
|
6d28eae5dc | ||
|
|
ca932ecadc | ||
|
|
04d763d814 | ||
|
|
97ea15e978 | ||
|
|
28c17f33ab | ||
|
|
db87a69488 | ||
|
|
2b154dcfdf | ||
|
|
1000039c74 | ||
|
|
2ccc543542 | ||
|
|
8ac2ee7b49 | ||
|
|
db3fd27da2 | ||
|
|
b871d2a8c4 | ||
|
|
02f348b15f | ||
|
|
4672f16fda | ||
|
|
6404657f99 | ||
|
|
2f3557c18f | ||
|
|
047f03a58c | ||
|
|
606b015314 | ||
|
|
478d82196a | ||
|
|
034ff2fb3f | ||
|
|
9bfb89fffd | ||
|
|
26ff33c41a | ||
|
|
9e84e3b83b | ||
|
|
82f5d0070e | ||
|
|
22d2289ed2 | ||
|
|
3ba18633de | ||
|
|
dde52309d2 | ||
|
|
87c13a876b | ||
|
|
0d01a3fce8 | ||
|
|
d4b96a5a27 | ||
|
|
1bbfc6da25 | ||
|
|
1aabb2ed5d | ||
|
|
f53810ebb2 | ||
|
|
7545599493 | ||
|
|
eecd51e08b | ||
|
|
3047208e6a | ||
|
|
a796e29c98 | ||
|
|
26abd1f4e2 | ||
|
|
5f0e04aebe | ||
|
|
918ca4fdcb | ||
|
|
24e5dd0c78 | ||
|
|
f35715683e | ||
|
|
b5a65bc66c | ||
|
|
3a21f7ab26 | ||
|
|
a2c012d2c4 | ||
|
|
4896059162 | ||
|
|
eadecf3eda | ||
|
|
39e4ab164e | ||
|
|
5089df3ffd | ||
|
|
9738257aa2 | ||
|
|
693b32077a | ||
|
|
47ea75348c | ||
|
|
cd9c425d9a | ||
|
|
6612e0107e | ||
|
|
ae0a481c64 | ||
|
|
1a43ef82d9 | ||
|
|
f2bc824a8e | ||
|
|
23a9cb2fad | ||
|
|
22a80c5558 | ||
|
|
14ef7fa869 | ||
|
|
068803dc86 | ||
|
|
ee7f43abe1 | ||
|
|
015e25c618 | ||
|
|
5c8694ad65 | ||
|
|
ff42c89cc4 | ||
|
|
856500b014 | ||
|
|
380f59ac0b | ||
|
|
d30874e0de | ||
|
|
28925c12eb | ||
|
|
31fcad4dbb | ||
|
|
e2a840ff0b | ||
|
|
0a40418614 | ||
|
|
90dbe9bf60 | ||
|
|
09dc271eec | ||
|
|
1f0a4cc548 | ||
|
|
c0b94e4e8e | ||
|
|
e404f4efd2 | ||
|
|
afa1449f89 | ||
|
|
d540728f50 | ||
|
|
12402f4c30 | ||
|
|
2bc524a2ee | ||
|
|
43f1fbf5b3 | ||
|
|
e983f44fd3 | ||
|
|
4d9da4a1f8 | ||
|
|
9d30712fb2 | ||
|
|
03b207d154 | ||
|
|
df6ad680ce | ||
|
|
b1b63513e7 | ||
|
|
22e30b93d5 | ||
|
|
c5e82a3895 | ||
|
|
4466458076 | ||
|
|
47a6fc19ca | ||
|
|
c891f43b14 | ||
|
|
1509650462 | ||
|
|
695e9ec5d7 | ||
|
|
0c8d45f679 | ||
|
|
d0ac4332ab | ||
|
|
08b9eedcec | ||
|
|
5f9e687c9c | ||
|
|
f1877fcea9 | ||
|
|
78d104e60c | ||
|
|
dd0185816c | ||
|
|
83eede8bc5 | ||
|
|
b22d4cb9ca | ||
|
|
de081d0205 | ||
|
|
f92569d468 | ||
|
|
bbbfc27593 | ||
|
|
d289b26034 | ||
|
|
babdc1db38 | ||
|
|
b559c827d2 | ||
|
|
268aa755c4 | ||
|
|
5d7935c855 | ||
|
|
6305a35a4f | ||
|
|
a0103036e4 | ||
|
|
b3b7e08c50 | ||
|
|
7dbeaf7a01 | ||
|
|
967073eaaf | ||
|
|
473d76c9d1 | ||
|
|
5bff50cade | ||
|
|
bfcbd9da6c | ||
|
|
2435c177f5 | ||
|
|
8e5aa69321 | ||
|
|
f3e4911c68 | ||
|
|
e8e4ff4111 | ||
|
|
523c32b8af | ||
|
|
5cdccb47f4 | ||
|
|
fa73bf8e87 | ||
|
|
5fe4e6cc96 | ||
|
|
f23900fc8c | ||
|
|
a0790e9119 | ||
|
|
236db7d42d | ||
|
|
5da7590eea | ||
|
|
df97eb2f72 | ||
|
|
32a0e519ad | ||
|
|
78d6561e39 | ||
|
|
9147823305 | ||
|
|
e7cf87be97 | ||
|
|
6e28490b84 | ||
|
|
fcc15d2e33 | ||
|
|
3522eafd2c |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -11,7 +11,7 @@ about: Report a reproducible bug in the current release of NetBox
|
|||||||
NetBox installation, or if you have a general question, DO NOT open an
|
NetBox installation, or if you have a general question, DO NOT open an
|
||||||
issue. Instead, post to our mailing list:
|
issue. Instead, post to our mailing list:
|
||||||
|
|
||||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
https://groups.google.com/g/netbox-discuss
|
||||||
|
|
||||||
Please describe the environment in which you are running NetBox. Be sure
|
Please describe the environment in which you are running NetBox. Be sure
|
||||||
that you are running an unmodified instance of the latest stable release
|
that you are running an unmodified instance of the latest stable release
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -5,5 +5,5 @@ contact_links:
|
|||||||
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
|
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
|
||||||
about: Please read through our contributing policy before opening an issue or pull request
|
about: Please read through our contributing policy before opening an issue or pull request
|
||||||
- name: 💬 Discussion Group
|
- name: 💬 Discussion Group
|
||||||
url: https://groups.google.com/forum/#!forum/netbox-discuss
|
url: https://groups.google.com/g/netbox-discuss
|
||||||
about: Join our discussion group for assistance with installation issues and other problems
|
about: Join our discussion group for assistance with installation issues and other problems
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -11,7 +11,7 @@ about: Propose a new NetBox feature or enhancement
|
|||||||
If you have a general idea or question, please post to our mailing list
|
If you have a general idea or question, please post to our mailing list
|
||||||
instead of opening an issue:
|
instead of opening an issue:
|
||||||
|
|
||||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
https://groups.google.com/g/netbox-discuss
|
||||||
|
|
||||||
NOTE: Due to an excessive backlog of feature requests, we are not currently
|
NOTE: Due to an excessive backlog of feature requests, we are not currently
|
||||||
accepting any proposals which significantly extend NetBox's feature scope.
|
accepting any proposals which significantly extend NetBox's feature scope.
|
||||||
@@ -21,8 +21,8 @@ about: Propose a new NetBox feature or enhancement
|
|||||||
before submitting a bug report.
|
before submitting a bug report.
|
||||||
-->
|
-->
|
||||||
### Environment
|
### Environment
|
||||||
* Python version: <!-- Example: 3.6.9 -->
|
* Python version:
|
||||||
* NetBox version: <!-- Example: 2.7.3 -->
|
* NetBox version:
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Describe in detail the new functionality you are proposing. Include any
|
Describe in detail the new functionality you are proposing. Include any
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ except to report bugs or request features.
|
|||||||
|
|
||||||
We have established a Google Groups Mailing List for issues and general
|
We have established a Google Groups Mailing List for issues and general
|
||||||
discussion. This is the best forum for obtaining assistance with NetBox
|
discussion. This is the best forum for obtaining assistance with NetBox
|
||||||
installation. You can find us [here](https://groups.google.com/forum/#!forum/netbox-discuss).
|
installation. You can find us [here](https://groups.google.com/g/netbox-discuss).
|
||||||
|
|
||||||
### Slack
|
### Slack
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ overlooked.
|
|||||||
* Official channels for communication include:
|
* Official channels for communication include:
|
||||||
|
|
||||||
* GitHub issues/pull requests
|
* GitHub issues/pull requests
|
||||||
* The [netbox-discuss](https://groups.google.com/forum/#!forum/netbox-discuss) mailing list
|
* The [netbox-discuss](https://groups.google.com/g/netbox-discuss) mailing list
|
||||||
* The **#netbox** channel on [NetworkToCode Slack](https://networktocode.slack.com/)
|
* The **#netbox** channel on [NetworkToCode Slack](https://networktocode.slack.com/)
|
||||||
|
|
||||||
* Maintainers with no substantial recorded activity in a 60-day period will be
|
* Maintainers with no substantial recorded activity in a 60-day period will be
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ complete list of requirements, see `requirements.txt`. The code is available [on
|
|||||||
|
|
||||||
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
|
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
|
||||||
|
|
||||||
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss),
|
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/g/netbox-discuss),
|
||||||
or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode.slack.com/)!
|
or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode.slack.com/)!
|
||||||
|
|
||||||
### Build Status
|
### Build Status
|
||||||
@@ -44,7 +44,7 @@ and run `upgrade.sh`.
|
|||||||
|
|
||||||
Feature requests and bug reports must be submitted as GiHub issues. (Please be
|
Feature requests and bug reports must be submitted as GiHub issues. (Please be
|
||||||
sure to use the [appropriate template](https://github.com/netbox-community/netbox/issues/new/choose).)
|
sure to use the [appropriate template](https://github.com/netbox-community/netbox/issues/new/choose).)
|
||||||
For general discussion, please consider joining our [mailing list](https://groups.google.com/forum/#!forum/netbox-discuss).
|
For general discussion, please consider joining our [mailing list](https://groups.google.com/g/netbox-discuss).
|
||||||
|
|
||||||
If you are interested in contributing to the development of NetBox, please read
|
If you are interested in contributing to the development of NetBox, please read
|
||||||
our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
|
our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ When viewing a device named Router4, this link would render as:
|
|||||||
|
|
||||||
Custom links appear as buttons at the top right corner of the page. Numeric weighting can be used to influence the ordering of links.
|
Custom links appear as buttons at the top right corner of the page. Numeric weighting can be used to influence the ordering of links.
|
||||||
|
|
||||||
|
## Context Data
|
||||||
|
|
||||||
|
The following context data is available within the template when rendering a custom link's text or URL.
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `obj` | The NetBox object being displayed |
|
||||||
|
| `debug` | A boolean indicating whether debugging is enabled |
|
||||||
|
| `request` | The current WSGI request |
|
||||||
|
| `user` | The current user (if authenticated) |
|
||||||
|
| `perms` | The [permissions](https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions) assigned to the user |
|
||||||
|
|
||||||
## Conditional Rendering
|
## Conditional Rendering
|
||||||
|
|
||||||
Only links which render with non-empty text are included on the page. You can employ conditional Jinja2 logic to control the conditions under which a link gets rendered.
|
Only links which render with non-empty text are included on the page. You can employ conditional Jinja2 logic to control the conditions under which a link gets rendered.
|
||||||
|
|||||||
@@ -231,6 +231,30 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
|
|||||||
* `min_prefix_length` - Minimum length of the mask
|
* `min_prefix_length` - Minimum length of the mask
|
||||||
* `max_prefix_length` - Maximum length of the mask
|
* `max_prefix_length` - Maximum length of the mask
|
||||||
|
|
||||||
|
## Running Custom Scripts
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Via the Web UI
|
||||||
|
|
||||||
|
Custom scripts can be run via the web UI by navigating to the script, completing any required form data, and clicking the "run script" button.
|
||||||
|
|
||||||
|
### Via the API
|
||||||
|
|
||||||
|
To run a script via the REST API, issue a POST request to the script's endpoint specifying the form data and commitment. For example, to run a script named `example.MyReport`, we would make a request such as the following:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Token $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json; indent=4" \
|
||||||
|
http://netbox/api/extras/scripts/example.MyReport/ \
|
||||||
|
--data '{"data": {"foo": "somevalue", "bar": 123}, "commit": true}'
|
||||||
|
```
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Below is an example script that creates new objects for a planned site. The user is prompted for three variables:
|
Below is an example script that creates new objects for a planned site. The user is prompted for three variables:
|
||||||
|
|||||||
@@ -101,11 +101,14 @@ Once you have created a report, it will appear in the reports list. Initially, r
|
|||||||
|
|
||||||
## Running Reports
|
## Running Reports
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
To run a report, a user must be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### Via the Web UI
|
### Via the Web UI
|
||||||
|
|
||||||
Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Note that a user must have permission to create ReportResults in order to run reports. (Permissions can be assigned through the admin UI.)
|
Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Once a report has been run, its associated results will be included in the report view.
|
||||||
|
|
||||||
Once a report has been run, its associated results will be included in the report view.
|
|
||||||
|
|
||||||
### Via the API
|
### Via the API
|
||||||
|
|
||||||
|
|||||||
@@ -491,6 +491,14 @@ The file path to the location where custom reports will be kept. By default, thi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## RQ_DEFAULT_TIMEOUT
|
||||||
|
|
||||||
|
Default: `300`
|
||||||
|
|
||||||
|
The maximum execution time of a background task (such as running a custom script), in seconds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## SCRIPTS_ROOT
|
## SCRIPTS_ROOT
|
||||||
|
|
||||||
Default: `$INSTALL_ROOT/netbox/scripts/`
|
Default: `$INSTALL_ROOT/netbox/scripts/`
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes
|
|||||||
* `PORT` - TCP port of the Redis service; leave blank for default port (6379)
|
* `PORT` - TCP port of the Redis service; leave blank for default port (6379)
|
||||||
* `PASSWORD` - Redis password (if set)
|
* `PASSWORD` - Redis password (if set)
|
||||||
* `DATABASE` - Numeric database ID
|
* `DATABASE` - Numeric database ID
|
||||||
* `DEFAULT_TIMEOUT` - Connection timeout in seconds
|
|
||||||
* `SSL` - Use SSL connection to Redis
|
* `SSL` - Use SSL connection to Redis
|
||||||
|
|
||||||
An example configuration is provided below:
|
An example configuration is provided below:
|
||||||
@@ -77,7 +76,6 @@ REDIS = {
|
|||||||
'PORT': 1234,
|
'PORT': 1234,
|
||||||
'PASSWORD': 'foobar',
|
'PASSWORD': 'foobar',
|
||||||
'DATABASE': 0,
|
'DATABASE': 0,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
},
|
},
|
||||||
'caching': {
|
'caching': {
|
||||||
@@ -85,7 +83,6 @@ REDIS = {
|
|||||||
'PORT': 6379,
|
'PORT': 6379,
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 1,
|
'DATABASE': 1,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +106,7 @@ above and the addition of two new keys.
|
|||||||
* `SENTINELS`: List of tuples or tuple of tuples with each inner tuple containing the name or IP address
|
* `SENTINELS`: List of tuples or tuple of tuples with each inner tuple containing the name or IP address
|
||||||
of the Redis server and port for each sentinel instance to connect to
|
of the Redis server and port for each sentinel instance to connect to
|
||||||
* `SENTINEL_SERVICE`: Name of the master / service to connect to
|
* `SENTINEL_SERVICE`: Name of the master / service to connect to
|
||||||
|
* `SENTINEL_TIMEOUT`: Connection timeout, in seconds
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -117,9 +115,9 @@ REDIS = {
|
|||||||
'tasks': {
|
'tasks': {
|
||||||
'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||||
'SENTINEL_SERVICE': 'netbox',
|
'SENTINEL_SERVICE': 'netbox',
|
||||||
|
'SENTINEL_TIMEOUT': 10,
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 0,
|
'DATABASE': 0,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
},
|
},
|
||||||
'caching': {
|
'caching': {
|
||||||
@@ -130,7 +128,6 @@ REDIS = {
|
|||||||
'SENTINEL_SERVICE': 'netbox',
|
'SENTINEL_SERVICE': 'netbox',
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 1,
|
'DATABASE': 1,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ NetBox is maintained as a [GitHub project](https://github.com/netbox-community/n
|
|||||||
Communication among developers should always occur via public channels:
|
Communication among developers should always occur via public channels:
|
||||||
|
|
||||||
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
|
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
|
||||||
* [The mailing list](https://groups.google.com/forum/#!forum/netbox-discuss) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
* [The mailing list](https://groups.google.com/g/netbox-discuss) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
||||||
* [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
* [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
||||||
|
|
||||||
## Governance
|
## Governance
|
||||||
|
|||||||
@@ -89,7 +89,3 @@ On the `develop` branch, update `VERSION` in `settings.py` to point to the next
|
|||||||
```
|
```
|
||||||
VERSION = 'v2.3.5-dev'
|
VERSION = 'v2.3.5-dev'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Announce the Release
|
|
||||||
|
|
||||||
Announce the release on the [mailing list](https://groups.google.com/forum/#!forum/netbox-discuss). Include a link to the release and the (HTML-formatted) release notes.
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Begin by installing all system packages required by NetBox and its dependencies.
|
|||||||
Before continuing with either platform, update pip (Python's package management tool) to its latest release:
|
Before continuing with either platform, update pip (Python's package management tool) to its latest release:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# pip install --upgrade pip
|
# pip3 install --upgrade pip
|
||||||
```
|
```
|
||||||
|
|
||||||
## Download NetBox
|
## Download NetBox
|
||||||
@@ -163,7 +163,6 @@ REDIS = {
|
|||||||
'PORT': 6379, # Redis port
|
'PORT': 6379, # Redis port
|
||||||
'PASSWORD': '', # Redis password (optional)
|
'PASSWORD': '', # Redis password (optional)
|
||||||
'DATABASE': 0, # Database ID
|
'DATABASE': 0, # Database ID
|
||||||
'DEFAULT_TIMEOUT': 300, # Timeout (seconds)
|
|
||||||
'SSL': False, # Use SSL (optional)
|
'SSL': False, # Use SSL (optional)
|
||||||
},
|
},
|
||||||
'caching': {
|
'caching': {
|
||||||
@@ -171,7 +170,6 @@ REDIS = {
|
|||||||
'PORT': 6379,
|
'PORT': 6379,
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 1, # Unique ID for second database
|
'DATABASE': 1, # Unique ID for second database
|
||||||
'DEFAULT_TIMEOUT': 300,
|
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,17 +143,28 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
|
|||||||
|
|
||||||
`systemctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
|
`systemctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
|
||||||
|
|
||||||
For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`.
|
For troubleshooting LDAP user/group queries, add or merge the following [logging](/configuration/optional-settings.md#logging) configuration to `configuration.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import logging, logging.handlers
|
LOGGING = {
|
||||||
logfile = "/opt/netbox/logs/django-ldap-debug.log"
|
'version': 1,
|
||||||
my_logger = logging.getLogger('django_auth_ldap')
|
'disable_existing_loggers': False,
|
||||||
my_logger.setLevel(logging.DEBUG)
|
'handlers': {
|
||||||
handler = logging.handlers.RotatingFileHandler(
|
'netbox_auth_log': {
|
||||||
logfile, maxBytes=1024 * 500, backupCount=5
|
'level': 'DEBUG',
|
||||||
)
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
my_logger.addHandler(handler)
|
'filename': '/opt/netbox/logs/django-ldap-debug.log',
|
||||||
|
'maxBytes': 1024 * 500,
|
||||||
|
'backupCount': 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django_auth_ldap': {
|
||||||
|
'handlers': ['netbox_auth_log'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Ensure the file and path specified in logfile exist and are writable and executable by the application service account. Restart the netbox service and attempt to log into the site to trigger log entries to this file.
|
Ensure the file and path specified in logfile exist and are writable and executable by the application service account. Restart the netbox service and attempt to log into the site to trigger log entries to this file.
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ You can use the command `systemctl status netbox` to verify that the WSGI servic
|
|||||||
# systemctl status netbox.service
|
# systemctl status netbox.service
|
||||||
● netbox.service - NetBox WSGI Service
|
● netbox.service - NetBox WSGI Service
|
||||||
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||||
Active: active (running) since Thu 2019-12-12 19:23:40 UTC; 25s ago
|
Active: active (running) since Sat 2020-10-24 19:23:40 UTC; 25s ago
|
||||||
Docs: https://netbox.readthedocs.io/en/stable/
|
Docs: https://netbox.readthedocs.io/en/stable/
|
||||||
Main PID: 11993 (gunicorn)
|
Main PID: 11993 (gunicorn)
|
||||||
Tasks: 6 (limit: 2362)
|
Tasks: 6 (limit: 2362)
|
||||||
CGroup: /system.slice/netbox.service
|
CGroup: /system.slice/netbox.service
|
||||||
├─11993 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
├─11993 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
||||||
├─12015 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
├─12015 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
||||||
├─12016 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
├─12016 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -28,44 +28,44 @@ Download and extract the latest version:
|
|||||||
# wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
|
# wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
|
||||||
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||||
# cd /opt/
|
# cd /opt/
|
||||||
# ln -sfn netbox-X.Y.Z/ netbox
|
# ln -sfn /opt/netbox-X.Y.Z/ /opt/netbox
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy the 'configuration.py' you created when first installing to the new version:
|
Copy the 'configuration.py' you created when first installing to the new version:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.py
|
# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy your local requirements file if used:
|
Copy your local requirements file if used:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp netbox-X.Y.Z/local_requirements.txt netbox/local_requirements.txt
|
# cp /opt/netbox-X.Y.Z/local_requirements.txt /opt/netbox/local_requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Also copy the LDAP configuration if using LDAP:
|
Also copy the LDAP configuration if using LDAP:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp netbox-X.Y.Z/netbox/netbox/ldap_config.py netbox/netbox/netbox/ldap_config.py
|
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.)
|
Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.)
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp -pr netbox-X.Y.Z/netbox/media/ netbox/netbox/
|
# cp -pr /opt/netbox-X.Y.Z/netbox/media/ /opt/netbox/netbox/
|
||||||
```
|
```
|
||||||
|
|
||||||
Also make sure to copy over any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.)
|
Also make sure to copy over any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.)
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp -r /opt/netbox-X.Y.Z/netbox/scripts /opt/netbox/netbox/scripts/
|
# cp -r /opt/netbox-X.Y.Z/netbox/scripts /opt/netbox/netbox/
|
||||||
# cp -r /opt/netbox-X.Y.Z/netbox/reports /opt/netbox/netbox/reports/
|
# cp -r /opt/netbox-X.Y.Z/netbox/reports /opt/netbox/netbox/
|
||||||
```
|
```
|
||||||
|
|
||||||
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cp netbox-X.Y.Z/gunicorn.py netbox/gunicorn.py
|
# cp /opt/netbox-X.Y.Z/gunicorn.py /opt/netbox/gunicorn.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option B: Clone the Git Repository
|
### Option B: Clone the Git Repository
|
||||||
|
|||||||
BIN
docs/media/admin_ui_run_permission.png
Normal file
BIN
docs/media/admin_ui_run_permission.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
@@ -1,3 +1,3 @@
|
|||||||
## Rear Port Templates
|
## Rear Port Templates
|
||||||
|
|
||||||
A template for a rear-facing pass-through port that will be created on all instantiations of the parent device type. Each rear port may have a physical type and one or more front port templates assigned to it. The number of positions associated with a rear port determines how many front ports can be assigned to it (the maximum is 64).
|
A template for a rear-facing pass-through port that will be created on all instantiations of the parent device type. Each rear port may have a physical type and one or more front port templates assigned to it. The number of positions associated with a rear port determines how many front ports can be assigned to it (the maximum is 1024).
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ Plugins can do a lot, including:
|
|||||||
|
|
||||||
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
|
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
While very powerful, the NetBox plugins API is necessarily limited in its scope. The plugins API is discussed here in its entirety: Any part of the NetBox code base not documented here is _not_ part of the supported plugins API, and should not be employed by a plugin. Internal elements of NetBox are subject to change at any time and without warning. Plugin authors are **strongly** encouraged to develop plugins using only the officially supported components discussed here and those provided by the underlying Django framework so as to avoid breaking changes in future releases.
|
||||||
|
|
||||||
## Initial Setup
|
## Initial Setup
|
||||||
|
|
||||||
## Plugin Structure
|
## Plugin Structure
|
||||||
@@ -198,14 +201,25 @@ class RandomAnimalView(View):
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
This view retrieves a random animal from the database and and passes it as a context variable when rendering a template named `animal.html`, which doesn't exist yet. To create this template, first create a directory named `templates/netbox_animal_sounds/` within the plugin source directory. (We use the plugin's name as a subdirectory to guard against naming collisions with other plugins.) Then, create `animal.html`:
|
This view retrieves a random animal from the database and and passes it as a context variable when rendering a template named `animal.html`, which doesn't exist yet. To create this template, first create a directory named `templates/netbox_animal_sounds/` within the plugin source directory. (We use the plugin's name as a subdirectory to guard against naming collisions with other plugins.) Then, create a template named `animal.html` as described below.
|
||||||
|
|
||||||
|
### Extending the Base Template
|
||||||
|
|
||||||
|
NetBox provides a base template to ensure a consistent user experience, which plugins can extend with their own content. This template includes four content blocks:
|
||||||
|
|
||||||
|
* `title` - The page title
|
||||||
|
* `header` - The upper portion of the page
|
||||||
|
* `content` - The main page body
|
||||||
|
* `javascript` - A section at the end of the page for including Javascript code
|
||||||
|
|
||||||
|
For more information on how template blocks work, consult the [Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#block).
|
||||||
|
|
||||||
```jinja2
|
```jinja2
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
|
{% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
|
||||||
<h2 class="text-center" style="margin-top: 200px">
|
<h2 class="text-center" style="margin-top: 200px">
|
||||||
{% if animal %}
|
{% if animal %}
|
||||||
The {{ animal.name|lower }} says
|
The {{ animal.name|lower }} says
|
||||||
{% if config.loud %}
|
{% if config.loud %}
|
||||||
@@ -216,8 +230,8 @@ This view retrieves a random animal from the database and and passes it as a con
|
|||||||
{% else %}
|
{% else %}
|
||||||
No animals have been created yet!
|
No animals have been created yet!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -328,6 +342,9 @@ A `PluginMenuButton` has the following attributes:
|
|||||||
* `color` - One of the choices provided by `ButtonColorChoices` (optional)
|
* `color` - One of the choices provided by `ButtonColorChoices` (optional)
|
||||||
* `permissions` - A list of permissions required to display this button (optional)
|
* `permissions` - A list of permissions required to display this button (optional)
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons.
|
||||||
|
|
||||||
## Extending Core Templates
|
## Extending Core Templates
|
||||||
|
|
||||||
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
|
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ PLUGINS_CONFIG = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Run Database Migrations
|
||||||
|
|
||||||
|
If the plugin introduces new database models, run the provided schema migrations:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
(venv) $ cd /opt/netbox/netbox/
|
||||||
|
(venv) $ python3 manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
### Collect Static Files
|
### Collect Static Files
|
||||||
|
|
||||||
Plugins may package static files to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
|
Plugins may package static files to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
|
||||||
|
|||||||
@@ -1,5 +1,145 @@
|
|||||||
# NetBox v2.9
|
# NetBox v2.9
|
||||||
|
|
||||||
|
## v2.9.8 (2020-10-30)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#4559](https://github.com/netbox-community/netbox/issues/4559) - Improve device/VM context data rendering performance
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#3672](https://github.com/netbox-community/netbox/issues/3672) - Fix a caching issue causing incorrect related object counts in API responses
|
||||||
|
* [#5113](https://github.com/netbox-community/netbox/issues/5113) - Fix incorrect caching of permission object assignments to user groups in the admin panel
|
||||||
|
* [#5243](https://github.com/netbox-community/netbox/issues/5243) - Redirect user to appropriate tab after modifying device components
|
||||||
|
* [#5273](https://github.com/netbox-community/netbox/issues/5273) - Fix exception when validating a new permission with no models selected
|
||||||
|
* [#5282](https://github.com/netbox-community/netbox/issues/5282) - Fix high CPU load when LDAP authentication is enabled
|
||||||
|
* [#5285](https://github.com/netbox-community/netbox/issues/5285) - Plugins no longer need to define `app_name` for API URLs to be included in the root view
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.9.7 (2020-10-12)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#5231](https://github.com/netbox-community/netbox/issues/5231) - Fix KeyError exception when viewing object with custom link and debugging is disabled
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.9.6 (2020-10-09)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#5229](https://github.com/netbox-community/netbox/issues/5229) - Fix AttributeError exception when LDAP authentication is enabled
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.9.5 (2020-10-09)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#5202](https://github.com/netbox-community/netbox/issues/5202) - Extend the available context data when rendering custom links
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#4523](https://github.com/netbox-community/netbox/issues/4523) - Populate site vlan list when bulk editing interfaces under certain circumstances
|
||||||
|
* [#5174](https://github.com/netbox-community/netbox/issues/5174) - Ensure consistent alignment of rack elevations
|
||||||
|
* [#5175](https://github.com/netbox-community/netbox/issues/5175) - Fix toggling of rack elevation order
|
||||||
|
* [#5184](https://github.com/netbox-community/netbox/issues/5184) - Fix missing Power Utilization
|
||||||
|
* [#5197](https://github.com/netbox-community/netbox/issues/5197) - Limit duplicate IPs shown on IP address view
|
||||||
|
* [#5199](https://github.com/netbox-community/netbox/issues/5199) - Change default LDAP logging to INFO
|
||||||
|
* [#5201](https://github.com/netbox-community/netbox/issues/5201) - Fix missing querystring when bulk editing/deleting VLAN Group VLANs when selecting "select all x items matching query"
|
||||||
|
* [#5206](https://github.com/netbox-community/netbox/issues/5206) - Apply user pagination preferences to all paginated object lists
|
||||||
|
* [#5211](https://github.com/netbox-community/netbox/issues/5211) - Add missing `has_primary_ip` filter for virtual machines
|
||||||
|
* [#5217](https://github.com/netbox-community/netbox/issues/5217) - Prevent erroneous removal of prefetched GenericForeignKey data from tables
|
||||||
|
* [#5218](https://github.com/netbox-community/netbox/issues/5218) - Raise validation error if a power port's `allocated_draw` exceeds its `maximum_draw`
|
||||||
|
* [#5220](https://github.com/netbox-community/netbox/issues/5220) - Fix API patch request against IP Address endpoint with null assigned_object_type
|
||||||
|
* [#5221](https://github.com/netbox-community/netbox/issues/5221) - Fix bulk component creation for virtual machines
|
||||||
|
* [#5224](https://github.com/netbox-community/netbox/issues/5224) - Don't allow a rear port to have fewer positions than the number of mapped front ports
|
||||||
|
* [#5226](https://github.com/netbox-community/netbox/issues/5226) - Custom choice fields should be blank initially if no default choice has been designated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.9.4 (2020-09-23)
|
||||||
|
|
||||||
|
**NOTE:** This release removes support for the `DEFAULT_TIMEOUT` parameter under `REDIS` database configuration. Set `RQ_DEFAULT_TIMEOUT` as a global configuration parameter instead.
|
||||||
|
|
||||||
|
**NOTE:** Any permissions referencing the legacy ReportResult model (e.g. `extras.view_reportresult`) should be updated to reference the Report model.
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#1755](https://github.com/netbox-community/netbox/issues/1755) - Toggle order in which rack elevations are displayed
|
||||||
|
* [#5128](https://github.com/netbox-community/netbox/issues/5128) - Increase maximum rear port positions from 64 to 1024
|
||||||
|
* [#5134](https://github.com/netbox-community/netbox/issues/5134) - Display full hierarchy in breadcrumbs for sites/racks
|
||||||
|
* [#5149](https://github.com/netbox-community/netbox/issues/5149) - Add rack group field to device edit form
|
||||||
|
* [#5164](https://github.com/netbox-community/netbox/issues/5164) - Show total rack count per rack group under site view
|
||||||
|
* [#5171](https://github.com/netbox-community/netbox/issues/5171) - Introduce the `RQ_DEFAULT_TIMEOUT` configuration parameter
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#5050](https://github.com/netbox-community/netbox/issues/5050) - Fix potential failure on `0016_replicate_interfaces` schema migration from old release
|
||||||
|
* [#5066](https://github.com/netbox-community/netbox/issues/5066) - Update `view_reportresult` to `view_report` permission
|
||||||
|
* [#5075](https://github.com/netbox-community/netbox/issues/5075) - Include a VLAN membership view for VM interfaces
|
||||||
|
* [#5105](https://github.com/netbox-community/netbox/issues/5105) - Validation should fail when reassigning a primary IP from device to VM
|
||||||
|
* [#5109](https://github.com/netbox-community/netbox/issues/5109) - Fix representation of custom choice field values for webhook data
|
||||||
|
* [#5108](https://github.com/netbox-community/netbox/issues/5108) - Fix execution of reports via CLI
|
||||||
|
* [#5111](https://github.com/netbox-community/netbox/issues/5111) - Allow use of tuples when specifying ObjectVar `query_params`
|
||||||
|
* [#5118](https://github.com/netbox-community/netbox/issues/5118) - Specifying an empty list of tags should clear assigned tags (REST API)
|
||||||
|
* [#5133](https://github.com/netbox-community/netbox/issues/5133) - Fix disassociation of an IP address from a VM interface
|
||||||
|
* [#5136](https://github.com/netbox-community/netbox/issues/5136) - Fix exception when bulk editing interface 802.1Q mode
|
||||||
|
* [#5156](https://github.com/netbox-community/netbox/issues/5156) - Add missing "add" button to rack reservations list
|
||||||
|
* [#5167](https://github.com/netbox-community/netbox/issues/5167) - Support filtering ObjectChanges by multiple users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.9.3 (2020-09-04)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#4977](https://github.com/netbox-community/netbox/issues/4977) - Redirect authenticated users from login view
|
||||||
|
* [#5048](https://github.com/netbox-community/netbox/issues/5048) - Show the device/VM name when editing a component
|
||||||
|
* [#5072](https://github.com/netbox-community/netbox/issues/5072) - Add REST API filters for image attachments
|
||||||
|
* [#5080](https://github.com/netbox-community/netbox/issues/5080) - Add 8P6C, 8P4C, 8P2C port types
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#5046](https://github.com/netbox-community/netbox/issues/5046) - Disabled plugin menu items are no longer clickable
|
||||||
|
* [#5063](https://github.com/netbox-community/netbox/issues/5063) - Fix "add device" link in rack elevations for opposite side of half-depth devices
|
||||||
|
* [#5074](https://github.com/netbox-community/netbox/issues/5074) - Fix inclusion of VC member interfaces when viewing VC master
|
||||||
|
* [#5078](https://github.com/netbox-community/netbox/issues/5078) - Fix assignment of existing IP addresses to interfaces via web UI
|
||||||
|
* [#5081](https://github.com/netbox-community/netbox/issues/5081) - Fix exception during webhook processing with custom select field
|
||||||
|
* [#5085](https://github.com/netbox-community/netbox/issues/5085) - Fix ordering by assignment in IP addresses table
|
||||||
|
* [#5087](https://github.com/netbox-community/netbox/issues/5087) - Restore label field when editing console server ports, power ports, and power outlets
|
||||||
|
* [#5089](https://github.com/netbox-community/netbox/issues/5089) - Redirect to device view after editing component
|
||||||
|
* [#5090](https://github.com/netbox-community/netbox/issues/5090) - Fix status display for console/power/interface connections
|
||||||
|
* [#5091](https://github.com/netbox-community/netbox/issues/5091) - Avoid KeyError when handling invalid table preferences
|
||||||
|
* [#5095](https://github.com/netbox-community/netbox/issues/5095) - Show assigned prefixes in VLANs list
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.9.2 (2020-08-27)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#5055](https://github.com/netbox-community/netbox/issues/5055) - Add tags column to device/VM component list tables
|
||||||
|
* [#5056](https://github.com/netbox-community/netbox/issues/5056) - Add interface and parent columns to IP address list
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#4988](https://github.com/netbox-community/netbox/issues/4988) - Fix ordering of rack reservations with identical creation times
|
||||||
|
* [#5002](https://github.com/netbox-community/netbox/issues/5002) - Correct OpenAPI definition for `available-prefixes` endpoint
|
||||||
|
* [#5035](https://github.com/netbox-community/netbox/issues/5035) - Fix exception when modifying an IP address assigned to a VM
|
||||||
|
* [#5038](https://github.com/netbox-community/netbox/issues/5038) - Fix validation of primary IPs assigned to virtual machines
|
||||||
|
* [#5040](https://github.com/netbox-community/netbox/issues/5040) - Limit SLAAC status to IPv6 addresses
|
||||||
|
* [#5041](https://github.com/netbox-community/netbox/issues/5041) - Fix form tabs when assigning an IP to a VM interface
|
||||||
|
* [#5042](https://github.com/netbox-community/netbox/issues/5042) - Fix display of SLAAC label for IP addresses status
|
||||||
|
* [#5045](https://github.com/netbox-community/netbox/issues/5045) - Allow assignment of interfaces to non-master VC peer LAG during import
|
||||||
|
* [#5058](https://github.com/netbox-community/netbox/issues/5058) - Correct URL for front rack elevation images when using external storage
|
||||||
|
* [#5059](https://github.com/netbox-community/netbox/issues/5059) - Fix inclusion of checkboxes for interfaces in virtual machine view
|
||||||
|
* [#5060](https://github.com/netbox-community/netbox/issues/5060) - Fix validation when bulk-importing child devices
|
||||||
|
* [#5061](https://github.com/netbox-community/netbox/issues/5061) - Allow adding/removing tags when bulk editing virtual machine interfaces
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.9.1 (2020-08-22)
|
## v2.9.1 (2020-08-22)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
@@ -72,6 +212,7 @@ Two new REST API endpoints have been added to facilitate the retrieval and manip
|
|||||||
* If using NetBox's built-in remote authentication backend, update `REMOTE_AUTH_BACKEND` to `'netbox.authentication.RemoteUserBackend'`, as the authentication class has moved.
|
* If using NetBox's built-in remote authentication backend, update `REMOTE_AUTH_BACKEND` to `'netbox.authentication.RemoteUserBackend'`, as the authentication class has moved.
|
||||||
* If using LDAP authentication, set `REMOTE_AUTH_BACKEND` to `'netbox.authentication.LDAPBackend'`. (LDAP configuration parameters in `ldap_config.py` remain unchanged.)
|
* If using LDAP authentication, set `REMOTE_AUTH_BACKEND` to `'netbox.authentication.LDAPBackend'`. (LDAP configuration parameters in `ldap_config.py` remain unchanged.)
|
||||||
* `REMOTE_AUTH_DEFAULT_PERMISSIONS` now takes a dictionary rather than a list. This is a mapping of permission names to a dictionary of constraining attributes, or `None`. For example, `['dcim.add_site', 'dcim.change_site']` would become `{'dcim.add_site': None, 'dcim.change_site': None}`.
|
* `REMOTE_AUTH_DEFAULT_PERMISSIONS` now takes a dictionary rather than a list. This is a mapping of permission names to a dictionary of constraining attributes, or `None`. For example, `['dcim.add_site', 'dcim.change_site']` would become `{'dcim.add_site': None, 'dcim.change_site': None}`.
|
||||||
|
* Backward compatibility for the old `webhooks` Redis queue name has been dropped. Ensure that your `REDIS` configuration parameter specifies both the `tasks` and `caching` databases.
|
||||||
|
|
||||||
### REST API Changes
|
### REST API Changes
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count, Prefetch
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from extras.models import Graph
|
from extras.models import Graph
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
@@ -45,7 +44,7 @@ class ProviderView(ObjectView):
|
|||||||
|
|
||||||
paginate = {
|
paginate = {
|
||||||
'paginator_class': EnhancedPaginator,
|
'paginator_class': EnhancedPaginator,
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
'per_page': get_paginate_count(request)
|
||||||
}
|
}
|
||||||
RequestConfig(request, paginate).configure(circuits_table)
|
RequestConfig(request, paginate).configure(circuits_table)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from dcim.models import (
|
|||||||
VirtualChassis,
|
VirtualChassis,
|
||||||
)
|
)
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
from extras.api.serializers import RenderedGraphSerializer
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||||
from extras.models import Graph
|
from extras.models import Graph
|
||||||
from ipam.models import Prefix, VLAN
|
from ipam.models import Prefix, VLAN
|
||||||
from utilities.api import (
|
from utilities.api import (
|
||||||
@@ -336,7 +336,7 @@ class PlatformViewSet(ModelViewSet):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceViewSet(CustomFieldModelViewSet):
|
class DeviceViewSet(CustomFieldModelViewSet, ConfigContextQuerySetMixin):
|
||||||
queryset = Device.objects.prefetch_related(
|
queryset = Device.objects.prefetch_related(
|
||||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
|
||||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
||||||
|
|||||||
@@ -814,6 +814,9 @@ class InterfaceModeChoices(ChoiceSet):
|
|||||||
class PortTypeChoices(ChoiceSet):
|
class PortTypeChoices(ChoiceSet):
|
||||||
|
|
||||||
TYPE_8P8C = '8p8c'
|
TYPE_8P8C = '8p8c'
|
||||||
|
TYPE_8P6C = '8p6c'
|
||||||
|
TYPE_8P4C = '8p4c'
|
||||||
|
TYPE_8P2C = '8p2c'
|
||||||
TYPE_110_PUNCH = '110-punch'
|
TYPE_110_PUNCH = '110-punch'
|
||||||
TYPE_BNC = 'bnc'
|
TYPE_BNC = 'bnc'
|
||||||
TYPE_MRJ21 = 'mrj21'
|
TYPE_MRJ21 = 'mrj21'
|
||||||
@@ -833,6 +836,9 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
'Copper',
|
'Copper',
|
||||||
(
|
(
|
||||||
(TYPE_8P8C, '8P8C'),
|
(TYPE_8P8C, '8P8C'),
|
||||||
|
(TYPE_8P6C, '8P6C'),
|
||||||
|
(TYPE_8P4C, '8P4C'),
|
||||||
|
(TYPE_8P2C, '8P2C'),
|
||||||
(TYPE_110_PUNCH, '110 Punch'),
|
(TYPE_110_PUNCH, '110 Punch'),
|
||||||
(TYPE_BNC, 'BNC'),
|
(TYPE_BNC, 'BNC'),
|
||||||
(TYPE_MRJ21, 'MRJ21'),
|
(TYPE_MRJ21, 'MRJ21'),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
|
|||||||
#
|
#
|
||||||
|
|
||||||
REARPORT_POSITIONS_MIN = 1
|
REARPORT_POSITIONS_MIN = 1
|
||||||
REARPORT_POSITIONS_MAX = 64
|
REARPORT_POSITIONS_MAX = 1024
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -94,8 +94,12 @@ class RackElevationSVG:
|
|||||||
|
|
||||||
# Embed front device type image if one exists
|
# Embed front device type image if one exists
|
||||||
if self.include_images and device.device_type.front_image:
|
if self.include_images and device.device_type.front_image:
|
||||||
url = '{}{}'.format(self.base_url, device.device_type.front_image.url)
|
image = drawing.image(
|
||||||
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
|
href=device.device_type.front_image.url,
|
||||||
|
insert=start,
|
||||||
|
size=end,
|
||||||
|
class_='device-image'
|
||||||
|
)
|
||||||
image.fit(scale='slice')
|
image.fit(scale='slice')
|
||||||
link.add(image)
|
link.add(image)
|
||||||
|
|
||||||
@@ -107,8 +111,12 @@ class RackElevationSVG:
|
|||||||
|
|
||||||
# Embed rear device type image if one exists
|
# Embed rear device type image if one exists
|
||||||
if self.include_images and device.device_type.rear_image:
|
if self.include_images and device.device_type.rear_image:
|
||||||
url = device.device_type.rear_image.url
|
image = drawing.image(
|
||||||
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
|
href=device.device_type.rear_image.url,
|
||||||
|
insert=start,
|
||||||
|
size=end,
|
||||||
|
class_='device-image'
|
||||||
|
)
|
||||||
image.fit(scale='slice')
|
image.fit(scale='slice')
|
||||||
drawing.add(image)
|
drawing.add(image)
|
||||||
|
|
||||||
@@ -141,7 +149,7 @@ class RackElevationSVG:
|
|||||||
unit_cursor = 0
|
unit_cursor = 0
|
||||||
for u in elevation:
|
for u in elevation:
|
||||||
o = other[unit_cursor]
|
o = other[unit_cursor]
|
||||||
if not u['device'] and o['device']:
|
if not u['device'] and o['device'] and o['device'].device_type.is_full_depth:
|
||||||
u['device'] = o['device']
|
u['device'] = o['device']
|
||||||
u['height'] = 1
|
u['height'] = 1
|
||||||
unit_cursor += u.get('height', 1)
|
unit_cursor += u.get('height', 1)
|
||||||
|
|||||||
@@ -662,16 +662,10 @@ class DeviceFilterSet(
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
def _has_primary_ip(self, queryset, name, value):
|
def _has_primary_ip(self, queryset, name, value):
|
||||||
|
params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
|
||||||
if value:
|
if value:
|
||||||
return queryset.filter(
|
return queryset.filter(params)
|
||||||
Q(primary_ip4__isnull=False) |
|
return queryset.exclude(params)
|
||||||
Q(primary_ip6__isnull=False)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return queryset.exclude(
|
|
||||||
Q(primary_ip4__isnull=False) |
|
|
||||||
Q(primary_ip6__isnull=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _virtual_chassis_member(self, queryset, name, value):
|
def _virtual_chassis_member(self, queryset, name, value):
|
||||||
return queryset.exclude(virtual_chassis__isnull=value)
|
return queryset.exclude(virtual_chassis__isnull=value)
|
||||||
|
|||||||
@@ -1680,12 +1680,21 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
'region_id': '$region'
|
'region_id': '$region'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
rack_group = DynamicModelChoiceField(
|
||||||
|
queryset=RackGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
display_field='display_name',
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
rack = DynamicModelChoiceField(
|
rack = DynamicModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
display_field='display_name',
|
display_field='display_name',
|
||||||
query_params={
|
query_params={
|
||||||
'site_id': '$site'
|
'site_id': '$site',
|
||||||
|
'group_id': '$rack_group',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
position = forms.TypedChoiceField(
|
position = forms.TypedChoiceField(
|
||||||
@@ -1811,7 +1820,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
nat_inside__assigned_object_id__in=interface_ids
|
nat_inside__assigned_object_id__in=interface_ids
|
||||||
).prefetch_related('assigned_object')
|
).prefetch_related('assigned_object')
|
||||||
if nat_ips:
|
if nat_ips:
|
||||||
ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in nat_ips]
|
ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
|
||||||
ip_choices.append(('NAT IPs', ip_list))
|
ip_choices.append(('NAT IPs', ip_list))
|
||||||
self.fields['primary_ip{}'.format(family)].choices = ip_choices
|
self.fields['primary_ip{}'.format(family)].choices = ip_choices
|
||||||
|
|
||||||
@@ -2317,7 +2326,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'type', 'description', 'tags',
|
'device', 'name', 'label', 'type', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
@@ -2390,7 +2399,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags',
|
'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
@@ -2479,7 +2488,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'tags',
|
'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
@@ -2835,6 +2844,24 @@ class InterfaceBulkEditForm(
|
|||||||
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
||||||
self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk)
|
self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk)
|
||||||
else:
|
else:
|
||||||
|
# See 4523
|
||||||
|
if 'pk' in self.initial:
|
||||||
|
site = None
|
||||||
|
interfaces = Interface.objects.filter(pk__in=self.initial['pk']).prefetch_related('device__site')
|
||||||
|
|
||||||
|
# Check interface sites. First interface should set site, further interfaces will either continue the
|
||||||
|
# loop or reset back to no site and break the loop.
|
||||||
|
for interface in interfaces:
|
||||||
|
if site is None:
|
||||||
|
site = interface.device.site
|
||||||
|
elif interface.device.site is not site:
|
||||||
|
site = None
|
||||||
|
break
|
||||||
|
|
||||||
|
if site is not None:
|
||||||
|
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
|
||||||
|
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
|
||||||
|
|
||||||
self.fields['lag'].choices = ()
|
self.fields['lag'].choices = ()
|
||||||
self.fields['lag'].widget.attrs['disabled'] = True
|
self.fields['lag'].widget.attrs['disabled'] = True
|
||||||
|
|
||||||
@@ -2879,17 +2906,22 @@ class InterfaceCSVForm(CSVModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit LAG choices to interfaces belonging to this device (or VC master)
|
# Limit LAG choices to interfaces belonging to this device (or virtual chassis)
|
||||||
device = None
|
device = None
|
||||||
if self.is_bound and 'device' in self.data:
|
if self.is_bound and 'device' in self.data:
|
||||||
try:
|
try:
|
||||||
device = self.fields['device'].to_python(self.data['device'])
|
device = self.fields['device'].to_python(self.data['device'])
|
||||||
except forms.ValidationError:
|
except forms.ValidationError:
|
||||||
pass
|
pass
|
||||||
|
if device and device.virtual_chassis:
|
||||||
if device:
|
|
||||||
self.fields['lag'].queryset = Interface.objects.filter(
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
device__in=[device, device.get_vc_master()], type=InterfaceTypeChoices.TYPE_LAG
|
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis),
|
||||||
|
type=InterfaceTypeChoices.TYPE_LAG
|
||||||
|
)
|
||||||
|
elif device:
|
||||||
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
|
device=device,
|
||||||
|
type=InterfaceTypeChoices.TYPE_LAG
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.fields['lag'].queryset = Interface.objects.none()
|
self.fields['lag'].queryset = Interface.objects.none()
|
||||||
|
|||||||
17
netbox/dcim/migrations/0115_rackreservation_order.py
Normal file
17
netbox/dcim/migrations/0115_rackreservation_order.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.1 on 2020-08-24 16:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0114_update_jsonfield'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rackreservation',
|
||||||
|
options={'ordering': ['created', 'pk']},
|
||||||
|
),
|
||||||
|
]
|
||||||
34
netbox/dcim/migrations/0116_rearport_max_positions.py
Normal file
34
netbox/dcim/migrations/0116_rearport_max_positions.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 3.1 on 2020-09-16 16:51
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0115_rackreservation_order'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='rear_port_position',
|
||||||
|
field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1024)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='frontporttemplate',
|
||||||
|
name='rear_port_position',
|
||||||
|
field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1024)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='positions',
|
||||||
|
field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1024)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rearporttemplate',
|
||||||
|
name='positions',
|
||||||
|
field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1024)]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -264,7 +264,10 @@ class FrontPortTemplate(ComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
rear_port_position = models.PositiveSmallIntegerField(
|
rear_port_position = models.PositiveSmallIntegerField(
|
||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[
|
||||||
|
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||||
|
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -315,7 +318,10 @@ class RearPortTemplate(ComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
positions = models.PositiveSmallIntegerField(
|
positions = models.PositiveSmallIntegerField(
|
||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[
|
||||||
|
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||||
|
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -405,6 +405,14 @@ class PowerPort(CableTermination, ComponentModel):
|
|||||||
self.description,
|
self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
if self.maximum_draw is not None and self.allocated_draw is not None:
|
||||||
|
if self.allocated_draw > self.maximum_draw:
|
||||||
|
raise ValidationError({
|
||||||
|
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
|
||||||
|
})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connected_endpoint(self):
|
def connected_endpoint(self):
|
||||||
"""
|
"""
|
||||||
@@ -809,7 +817,10 @@ class FrontPort(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
rear_port_position = models.PositiveSmallIntegerField(
|
rear_port_position = models.PositiveSmallIntegerField(
|
||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[
|
||||||
|
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||||
|
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
@@ -840,17 +851,16 @@ class FrontPort(CableTermination, ComponentModel):
|
|||||||
|
|
||||||
# Validate rear port assignment
|
# Validate rear port assignment
|
||||||
if self.rear_port.device != self.device:
|
if self.rear_port.device != self.device:
|
||||||
raise ValidationError(
|
raise ValidationError({
|
||||||
"Rear port ({}) must belong to the same device".format(self.rear_port)
|
"rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
|
||||||
)
|
})
|
||||||
|
|
||||||
# Validate rear port position assignment
|
# Validate rear port position assignment
|
||||||
if self.rear_port_position > self.rear_port.positions:
|
if self.rear_port_position > self.rear_port.positions:
|
||||||
raise ValidationError(
|
raise ValidationError({
|
||||||
"Invalid rear port position ({}); rear port {} has only {} positions".format(
|
"rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
|
||||||
self.rear_port_position, self.rear_port.name, self.rear_port.positions
|
f"{self.rear_port.name} has only {self.rear_port.positions} positions"
|
||||||
)
|
})
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
@extras_features('webhooks')
|
||||||
@@ -864,7 +874,10 @@ class RearPort(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
positions = models.PositiveSmallIntegerField(
|
positions = models.PositiveSmallIntegerField(
|
||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[
|
||||||
|
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||||
|
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
@@ -877,6 +890,16 @@ class RearPort(CableTermination, ComponentModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:rearport', kwargs={'pk': self.pk})
|
return reverse('dcim:rearport', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Check that positions count is greater than or equal to the number of associated FrontPorts
|
||||||
|
frontport_count = self.frontports.count()
|
||||||
|
if self.positions < frontport_count:
|
||||||
|
raise ValidationError({
|
||||||
|
"positions": f"The number of positions cannot be less than the number of mapped front ports "
|
||||||
|
f"({frontport_count})"
|
||||||
|
})
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.device.identifier,
|
self.device.identifier,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from taggit.managers import TaggableManager
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
|
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
|
||||||
|
from extras.querysets import ConfigContextModelQuerySet
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
@@ -594,7 +595,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = ConfigContextModelQuerySet.as_manager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
@@ -633,7 +634,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
# Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
|
# Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
|
||||||
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
||||||
# of the uniqueness constraint without manual intervention.
|
# of the uniqueness constraint without manual intervention.
|
||||||
if self.name and self.tenant is None:
|
if self.name and hasattr(self, 'site') and self.tenant is None:
|
||||||
if Device.objects.exclude(pk=self.pk).filter(
|
if Device.objects.exclude(pk=self.pk).filter(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
site=self.site,
|
site=self.site,
|
||||||
|
|||||||
@@ -600,7 +600,7 @@ class RackReservation(ChangeLoggedModel):
|
|||||||
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['created']
|
ordering = ['created', 'pk']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Reservation for rack {}".format(self.rack)
|
return "Reservation for rack {}".format(self.rack)
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ INTERFACE_TAGGED_VLANS = """
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
CONNECTION_STATUS = """
|
||||||
|
<span class="label label-{% if record.connection_status %}success{% else %}danger{% endif %}">{{ record.get_connection_status_display }}</span>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Regions
|
# Regions
|
||||||
@@ -428,7 +432,8 @@ class ComponentTemplateTable(BaseTable):
|
|||||||
class ConsolePortTemplateTable(ComponentTemplateTable):
|
class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=ConsolePortTemplate,
|
model=ConsolePortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_consoleports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -440,7 +445,8 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
|
|||||||
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=ConsoleServerPortTemplate,
|
model=ConsoleServerPortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_consoleserverports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -452,7 +458,8 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
|||||||
class PowerPortTemplateTable(ComponentTemplateTable):
|
class PowerPortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=PowerPortTemplate,
|
model=PowerPortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_powerports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -464,7 +471,8 @@ class PowerPortTemplateTable(ComponentTemplateTable):
|
|||||||
class PowerOutletTemplateTable(ComponentTemplateTable):
|
class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=PowerOutletTemplate,
|
model=PowerOutletTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_poweroutlets'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -479,7 +487,8 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
|||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=InterfaceTemplate,
|
model=InterfaceTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_interfaces'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -494,7 +503,8 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
|||||||
)
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=FrontPortTemplate,
|
model=FrontPortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_frontports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -506,7 +516,8 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
|||||||
class RearPortTemplateTable(ComponentTemplateTable):
|
class RearPortTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=RearPortTemplate,
|
model=RearPortTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_rearports'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -518,7 +529,8 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
|||||||
class DeviceBayTemplateTable(ComponentTemplateTable):
|
class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=DeviceBayTemplate,
|
model=DeviceBayTemplate,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
return_url_extra='%23tab_devicebays'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@@ -706,34 +718,48 @@ class DeviceComponentTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class ConsolePortTable(DeviceComponentTable):
|
class ConsolePortTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:consoleport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTable(DeviceComponentTable):
|
class ConsoleServerPortTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:consoleserverport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTable(DeviceComponentTable):
|
class PowerPortTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:powerport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable')
|
fields = (
|
||||||
|
'pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable', 'tags',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTable(DeviceComponentTable):
|
class PowerOutletTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:poweroutlet_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable')
|
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||||
|
|
||||||
|
|
||||||
@@ -753,12 +779,15 @@ class BaseInterfaceTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable):
|
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:interface_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||||
'description', 'cable', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
'description', 'cable', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
|
||||||
|
|
||||||
@@ -767,18 +796,26 @@ class FrontPortTable(DeviceComponentTable):
|
|||||||
rear_port_position = tables.Column(
|
rear_port_position = tables.Column(
|
||||||
verbose_name='Position'
|
verbose_name='Position'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:frontport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable')
|
fields = (
|
||||||
|
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||||
|
|
||||||
|
|
||||||
class RearPortTable(DeviceComponentTable):
|
class RearPortTable(DeviceComponentTable):
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:rearport_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable')
|
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
@@ -786,10 +823,13 @@ class DeviceBayTable(DeviceComponentTable):
|
|||||||
installed_device = tables.Column(
|
installed_device = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:devicebay_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description', 'tags')
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||||
|
|
||||||
|
|
||||||
@@ -798,12 +838,16 @@ class InventoryItemTable(DeviceComponentTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
discovered = BooleanColumn()
|
discovered = BooleanColumn()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:inventoryitem_list'
|
||||||
|
)
|
||||||
|
cable = None # Override DeviceComponentTable
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
'pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||||
'discovered',
|
'discovered', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
default_columns = ('pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||||
|
|
||||||
@@ -876,15 +920,20 @@ class ConsoleConnectionTable(BaseTable):
|
|||||||
verbose_name='Console Server'
|
verbose_name='Console Server'
|
||||||
)
|
)
|
||||||
connected_endpoint = tables.Column(
|
connected_endpoint = tables.Column(
|
||||||
|
linkify=True,
|
||||||
verbose_name='Port'
|
verbose_name='Port'
|
||||||
)
|
)
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
|
linkify=True,
|
||||||
verbose_name='Console Port'
|
verbose_name='Console Port'
|
||||||
)
|
)
|
||||||
connection_status = BooleanColumn()
|
connection_status = tables.TemplateColumn(
|
||||||
|
template_code=CONNECTION_STATUS,
|
||||||
|
verbose_name='Status'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
@@ -901,14 +950,20 @@ class PowerConnectionTable(BaseTable):
|
|||||||
)
|
)
|
||||||
outlet = tables.Column(
|
outlet = tables.Column(
|
||||||
accessor=Accessor('_connected_poweroutlet'),
|
accessor=Accessor('_connected_poweroutlet'),
|
||||||
|
linkify=True,
|
||||||
verbose_name='Outlet'
|
verbose_name='Outlet'
|
||||||
)
|
)
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
|
linkify=True,
|
||||||
verbose_name='Power Port'
|
verbose_name='Power Port'
|
||||||
)
|
)
|
||||||
|
connection_status = tables.TemplateColumn(
|
||||||
|
template_code=CONNECTION_STATUS,
|
||||||
|
verbose_name='Status'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
@@ -940,6 +995,10 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
args=[Accessor('_connected_interface__pk')],
|
args=[Accessor('_connected_interface__pk')],
|
||||||
verbose_name='Interface B'
|
verbose_name='Interface B'
|
||||||
)
|
)
|
||||||
|
connection_status = tables.TemplateColumn(
|
||||||
|
template_code=CONNECTION_STATUS,
|
||||||
|
verbose_name='Status'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||||
@@ -20,7 +19,7 @@ from ipam.models import IPAddress, Prefix, Service, VLAN
|
|||||||
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
||||||
from secrets.models import Secret
|
from secrets.models import Secret
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.utils import csv_format, get_subquery
|
from utilities.utils import csv_format, get_subquery
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
@@ -169,9 +168,13 @@ class SiteView(ObjectView):
|
|||||||
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=site).count(),
|
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=site).count(),
|
||||||
'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=site).count(),
|
'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=site).count(),
|
||||||
}
|
}
|
||||||
rack_groups = RackGroup.objects.restrict(request.user, 'view').filter(site=site).annotate(
|
rack_groups = RackGroup.objects.add_related_count(
|
||||||
rack_count=Count('racks')
|
RackGroup.objects.all(),
|
||||||
)
|
Rack,
|
||||||
|
'group',
|
||||||
|
'rack_count',
|
||||||
|
cumulative=True
|
||||||
|
).restrict(request.user, 'view').filter(site=site)
|
||||||
show_graphs = Graph.objects.filter(type__model='site').exists()
|
show_graphs = Graph.objects.filter(type__model='site').exists()
|
||||||
|
|
||||||
return render(request, 'dcim/site.html', {
|
return render(request, 'dcim/site.html', {
|
||||||
@@ -310,8 +313,13 @@ class RackElevationListView(ObjectListView):
|
|||||||
racks = filters.RackFilterSet(request.GET, self.queryset).qs
|
racks = filters.RackFilterSet(request.GET, self.queryset).qs
|
||||||
total_count = racks.count()
|
total_count = racks.count()
|
||||||
|
|
||||||
|
# Determine ordering
|
||||||
|
reverse = bool(request.GET.get('reverse', False))
|
||||||
|
if reverse:
|
||||||
|
racks = racks.reverse()
|
||||||
|
|
||||||
# Pagination
|
# Pagination
|
||||||
per_page = request.GET.get('per_page', settings.PAGINATE_COUNT)
|
per_page = get_paginate_count(request)
|
||||||
page_number = request.GET.get('page', 1)
|
page_number = request.GET.get('page', 1)
|
||||||
paginator = EnhancedPaginator(racks, per_page)
|
paginator = EnhancedPaginator(racks, per_page)
|
||||||
try:
|
try:
|
||||||
@@ -330,6 +338,7 @@ class RackElevationListView(ObjectListView):
|
|||||||
'paginator': paginator,
|
'paginator': paginator,
|
||||||
'page': page,
|
'page': page,
|
||||||
'total_count': total_count,
|
'total_count': total_count,
|
||||||
|
'reverse': reverse,
|
||||||
'rack_face': rack_face,
|
'rack_face': rack_face,
|
||||||
'filter_form': forms.RackElevationFilterForm(request.GET),
|
'filter_form': forms.RackElevationFilterForm(request.GET),
|
||||||
})
|
})
|
||||||
@@ -408,7 +417,6 @@ class RackReservationListView(ObjectListView):
|
|||||||
filterset = filters.RackReservationFilterSet
|
filterset = filters.RackReservationFilterSet
|
||||||
filterset_form = forms.RackReservationFilterForm
|
filterset_form = forms.RackReservationFilterForm
|
||||||
table = tables.RackReservationTable
|
table = tables.RackReservationTable
|
||||||
action_buttons = ('export',)
|
|
||||||
|
|
||||||
|
|
||||||
class RackReservationView(ObjectView):
|
class RackReservationView(ObjectView):
|
||||||
@@ -1033,7 +1041,7 @@ class DeviceView(ObjectView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Interfaces
|
# Interfaces
|
||||||
interfaces = device.vc_interfaces.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related(
|
||||||
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
||||||
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable',
|
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable',
|
||||||
@@ -1155,7 +1163,7 @@ class DeviceConfigView(ObjectView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceConfigContextView(ObjectConfigContextView):
|
class DeviceConfigContextView(ObjectConfigContextView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.annotate_config_context_data()
|
||||||
base_template = 'dcim/device.html'
|
base_template = 'dcim/device.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -1233,6 +1241,7 @@ class ConsolePortCreateView(ComponentCreateView):
|
|||||||
class ConsolePortEditView(ObjectEditView):
|
class ConsolePortEditView(ObjectEditView):
|
||||||
queryset = ConsolePort.objects.all()
|
queryset = ConsolePort.objects.all()
|
||||||
model_form = forms.ConsolePortForm
|
model_form = forms.ConsolePortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortDeleteView(ObjectDeleteView):
|
class ConsolePortDeleteView(ObjectDeleteView):
|
||||||
@@ -1292,6 +1301,7 @@ class ConsoleServerPortCreateView(ComponentCreateView):
|
|||||||
class ConsoleServerPortEditView(ObjectEditView):
|
class ConsoleServerPortEditView(ObjectEditView):
|
||||||
queryset = ConsoleServerPort.objects.all()
|
queryset = ConsoleServerPort.objects.all()
|
||||||
model_form = forms.ConsoleServerPortForm
|
model_form = forms.ConsoleServerPortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortDeleteView(ObjectDeleteView):
|
class ConsoleServerPortDeleteView(ObjectDeleteView):
|
||||||
@@ -1351,6 +1361,7 @@ class PowerPortCreateView(ComponentCreateView):
|
|||||||
class PowerPortEditView(ObjectEditView):
|
class PowerPortEditView(ObjectEditView):
|
||||||
queryset = PowerPort.objects.all()
|
queryset = PowerPort.objects.all()
|
||||||
model_form = forms.PowerPortForm
|
model_form = forms.PowerPortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class PowerPortDeleteView(ObjectDeleteView):
|
class PowerPortDeleteView(ObjectDeleteView):
|
||||||
@@ -1410,6 +1421,7 @@ class PowerOutletCreateView(ComponentCreateView):
|
|||||||
class PowerOutletEditView(ObjectEditView):
|
class PowerOutletEditView(ObjectEditView):
|
||||||
queryset = PowerOutlet.objects.all()
|
queryset = PowerOutlet.objects.all()
|
||||||
model_form = forms.PowerOutletForm
|
model_form = forms.PowerOutletForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletDeleteView(ObjectDeleteView):
|
class PowerOutletDeleteView(ObjectDeleteView):
|
||||||
@@ -1561,6 +1573,7 @@ class FrontPortCreateView(ComponentCreateView):
|
|||||||
class FrontPortEditView(ObjectEditView):
|
class FrontPortEditView(ObjectEditView):
|
||||||
queryset = FrontPort.objects.all()
|
queryset = FrontPort.objects.all()
|
||||||
model_form = forms.FrontPortForm
|
model_form = forms.FrontPortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class FrontPortDeleteView(ObjectDeleteView):
|
class FrontPortDeleteView(ObjectDeleteView):
|
||||||
@@ -1620,6 +1633,7 @@ class RearPortCreateView(ComponentCreateView):
|
|||||||
class RearPortEditView(ObjectEditView):
|
class RearPortEditView(ObjectEditView):
|
||||||
queryset = RearPort.objects.all()
|
queryset = RearPort.objects.all()
|
||||||
model_form = forms.RearPortForm
|
model_form = forms.RearPortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class RearPortDeleteView(ObjectDeleteView):
|
class RearPortDeleteView(ObjectDeleteView):
|
||||||
@@ -1679,6 +1693,7 @@ class DeviceBayCreateView(ComponentCreateView):
|
|||||||
class DeviceBayEditView(ObjectEditView):
|
class DeviceBayEditView(ObjectEditView):
|
||||||
queryset = DeviceBay.objects.all()
|
queryset = DeviceBay.objects.all()
|
||||||
model_form = forms.DeviceBayForm
|
model_form = forms.DeviceBayForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayDeleteView(ObjectDeleteView):
|
class DeviceBayDeleteView(ObjectDeleteView):
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
|||||||
instance.custom_fields = {}
|
instance.custom_fields = {}
|
||||||
for field in custom_fields:
|
for field in custom_fields:
|
||||||
value = instance.cf.get(field.name)
|
value = instance.cf.get(field.name)
|
||||||
if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
|
if field.type == CustomFieldTypeChoices.TYPE_SELECT and type(value) is CustomFieldChoice:
|
||||||
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
|
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
|
||||||
else:
|
else:
|
||||||
instance.custom_fields[field.name] = value
|
instance.custom_fields[field.name] = value
|
||||||
|
|||||||
@@ -101,24 +101,30 @@ class TaggedObjectSerializer(serializers.Serializer):
|
|||||||
tags = NestedTagSerializer(many=True, required=False)
|
tags = NestedTagSerializer(many=True, required=False)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
tags = validated_data.pop('tags', [])
|
tags = validated_data.pop('tags', None)
|
||||||
instance = super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
|
|
||||||
|
if tags is not None:
|
||||||
return self._save_tags(instance, tags)
|
return self._save_tags(instance, tags)
|
||||||
|
return instance
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
tags = validated_data.pop('tags', [])
|
tags = validated_data.pop('tags', None)
|
||||||
|
|
||||||
# Cache tags on instance for change logging
|
# Cache tags on instance for change logging
|
||||||
instance._tags = tags
|
instance._tags = tags or []
|
||||||
|
|
||||||
instance = super().update(instance, validated_data)
|
instance = super().update(instance, validated_data)
|
||||||
|
|
||||||
|
if tags is not None:
|
||||||
return self._save_tags(instance, tags)
|
return self._save_tags(instance, tags)
|
||||||
|
return instance
|
||||||
|
|
||||||
def _save_tags(self, instance, tags):
|
def _save_tags(self, instance, tags):
|
||||||
if tags:
|
if tags:
|
||||||
instance.tags.set(*[t.name for t in tags])
|
instance.tags.set(*[t.name for t in tags])
|
||||||
|
else:
|
||||||
|
instance.tags.clear()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,29 @@ from utilities.utils import copy_safe_request
|
|||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextQuerySetMixin:
|
||||||
|
"""
|
||||||
|
Used by views that work with config context models (device and virtual machine).
|
||||||
|
Provides a get_queryset() method which deals with adding the config context
|
||||||
|
data annotation or not.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
Build the proper queryset based on the request context
|
||||||
|
|
||||||
|
If the `brief` query param equates to True or the `exclude` query param
|
||||||
|
includes `config_context` as a value, return the base queryset.
|
||||||
|
|
||||||
|
Else, return the queryset annotated with config context data
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = self.get_serializer_context()['request']
|
||||||
|
if request.query_params.get('brief') or 'config_context' in request.query_params.get('exclude', []):
|
||||||
|
return self.queryset
|
||||||
|
return self.queryset.annotate_config_context_data()
|
||||||
|
|
||||||
|
|
||||||
class ExtrasRootView(APIRootView):
|
class ExtrasRootView(APIRootView):
|
||||||
"""
|
"""
|
||||||
Extras API root view
|
Extras API root view
|
||||||
@@ -140,6 +163,7 @@ class ImageAttachmentViewSet(ModelViewSet):
|
|||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
serializer_class = serializers.ImageAttachmentSerializer
|
serializer_class = serializers.ImageAttachmentSerializer
|
||||||
|
filterset_class = filters.ImageAttachmentFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ from tenancy.models import Tenant, TenantGroup
|
|||||||
from utilities.filters import BaseFilterSet
|
from utilities.filters import BaseFilterSet
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, JobResult, Tag
|
from .models import ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, JobResult, ObjectChange, Tag
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -17,6 +18,7 @@ __all__ = (
|
|||||||
'CustomFieldFilterSet',
|
'CustomFieldFilterSet',
|
||||||
'ExportTemplateFilterSet',
|
'ExportTemplateFilterSet',
|
||||||
'GraphFilterSet',
|
'GraphFilterSet',
|
||||||
|
'ImageAttachmentFilterSet',
|
||||||
'LocalConfigContextFilterSet',
|
'LocalConfigContextFilterSet',
|
||||||
'ObjectChangeFilterSet',
|
'ObjectChangeFilterSet',
|
||||||
'TagFilterSet',
|
'TagFilterSet',
|
||||||
@@ -104,6 +106,13 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
|||||||
fields = ['id', 'content_type', 'name', 'template_language']
|
fields = ['id', 'content_type', 'name', 'template_language']
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ImageAttachment
|
||||||
|
fields = ['id', 'content_type', 'object_id', 'name']
|
||||||
|
|
||||||
|
|
||||||
class TagFilterSet(BaseFilterSet):
|
class TagFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
@@ -251,12 +260,21 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
time = django_filters.DateTimeFromToRangeFilter()
|
time = django_filters.DateTimeFromToRangeFilter()
|
||||||
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
label='User (ID)',
|
||||||
|
)
|
||||||
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='user__username',
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
to_field_name='username',
|
||||||
|
label='User name',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ObjectChange
|
model = ObjectChange
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
|
'id', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id', 'object_repr',
|
||||||
'object_repr',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
|
|||||||
@@ -397,10 +397,11 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
user = DynamicModelMultipleChoiceField(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
display_field='username',
|
display_field='username',
|
||||||
|
label='User',
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/users/users/',
|
api_url='/api/users/users/',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
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.reports import get_reports
|
from extras.choices import JobResultStatusChoices
|
||||||
|
from extras.models import JobResult
|
||||||
|
from extras.reports import get_reports, run_report
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@@ -20,15 +25,33 @@ class Command(BaseCommand):
|
|||||||
for report in report_list:
|
for report in report_list:
|
||||||
if module_name in options['reports'] or report.full_name in options['reports']:
|
if module_name in options['reports'] or report.full_name in options['reports']:
|
||||||
|
|
||||||
# Run the report and create a new ReportResult
|
# Run the report and create a new JobResult
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
"[{:%H:%M:%S}] Running {}...".format(timezone.now(), report.full_name)
|
"[{:%H:%M:%S}] Running {}...".format(timezone.now(), report.full_name)
|
||||||
)
|
)
|
||||||
report.run()
|
|
||||||
|
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
||||||
|
job_result = JobResult.enqueue_job(
|
||||||
|
run_report,
|
||||||
|
report.full_name,
|
||||||
|
report_content_type,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait on the job to finish
|
||||||
|
while job_result.status not in JobResultStatusChoices.TERMINAL_STATE_CHOICES:
|
||||||
|
time.sleep(1)
|
||||||
|
job_result = JobResult.objects.get(pk=job_result.pk)
|
||||||
|
|
||||||
# Report on success/failure
|
# Report on success/failure
|
||||||
status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
|
if job_result.status == JobResultStatusChoices.STATUS_FAILED:
|
||||||
for test_name, attrs in report.result.data.items():
|
status = self.style.ERROR('FAILED')
|
||||||
|
elif job_result == JobResultStatusChoices.STATUS_ERRORED:
|
||||||
|
status = self.style.ERROR('ERRORED')
|
||||||
|
else:
|
||||||
|
status = self.style.SUCCESS('SUCCESS')
|
||||||
|
|
||||||
|
for test_name, attrs in job_result.data.items():
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
"\t{}: {} success, {} info, {} warning, {} failure".format(
|
"\t{}: {} success, {} info, {} warning, {} failure".format(
|
||||||
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failure']
|
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failure']
|
||||||
@@ -37,6 +60,9 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
"[{:%H:%M:%S}] {}: {}".format(timezone.now(), report.full_name, status)
|
"[{:%H:%M:%S}] {}: {}".format(timezone.now(), report.full_name, status)
|
||||||
)
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
"[{:%H:%M:%S}] {}: Duration {}".format(timezone.now(), report.full_name, job_result.duration)
|
||||||
|
)
|
||||||
|
|
||||||
# Wrap things up
|
# Wrap things up
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
|
|||||||
@@ -200,14 +200,13 @@ class CustomField(models.Model):
|
|||||||
# Select
|
# Select
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()]
|
choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()]
|
||||||
|
default_choice = self.choices.filter(value=self.default).first()
|
||||||
|
|
||||||
if not required:
|
if not required or default_choice is None:
|
||||||
choices = add_blank_choice(choices)
|
choices = add_blank_choice(choices)
|
||||||
|
|
||||||
# Set the initial value to the PK of the default choice, if any
|
# Set the initial value to the PK of the default choice, if any
|
||||||
if set_initial:
|
if set_initial and default_choice:
|
||||||
default_choice = self.choices.filter(value=self.default).first()
|
|
||||||
if default_choice:
|
|
||||||
initial = default_choice.pk
|
initial = default_choice.pk
|
||||||
|
|
||||||
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
|
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
|
||||||
|
|||||||
@@ -542,8 +542,16 @@ class ConfigContextModel(models.Model):
|
|||||||
|
|
||||||
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
for context in ConfigContext.objects.get_for_object(self):
|
|
||||||
data = deepmerge(data, context.data)
|
if not hasattr(self, 'config_context_data'):
|
||||||
|
# The annotation is not available, so we fall back to manually querying for the config context objects
|
||||||
|
config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True)
|
||||||
|
else:
|
||||||
|
# The attribute may exist, but the annotated value could be None if there is no config context data
|
||||||
|
config_context_data = self.config_context_data or []
|
||||||
|
|
||||||
|
for context in config_context_data:
|
||||||
|
data = deepmerge(data, context)
|
||||||
|
|
||||||
# If the object has local config context data defined, merge it last
|
# If the object has local config context data defined, merge it last
|
||||||
if self.local_context_data:
|
if self.local_context_data:
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from extras.plugins.utils import import_object
|
|
||||||
|
|
||||||
|
|
||||||
class InstalledPluginsAdminView(View):
|
class InstalledPluginsAdminView(View):
|
||||||
"""
|
"""
|
||||||
@@ -62,11 +60,7 @@ class PluginsAPIRootView(APIView):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_plugin_entry(plugin, app_config, request, format):
|
def _get_plugin_entry(plugin, app_config, request, format):
|
||||||
# Check if the plugin specifies any API URLs
|
# Check if the plugin specifies any API URLs
|
||||||
api_app_name = import_object(f"{plugin}.api.urls.app_name")
|
api_app_name = f'{app_config.name}-api'
|
||||||
if api_app_name is None:
|
|
||||||
# Plugin does not expose an API
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
entry = (getattr(app_config, 'base_url', app_config.label), reverse(
|
entry = (getattr(app_config, 'base_url', app_config.label), reverse(
|
||||||
f"plugins-api:{api_app_name}:api-root",
|
f"plugins-api:{api_app_name}:api-root",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.db.models import Q, QuerySet
|
from django.db.models import OuterRef, Subquery, Q
|
||||||
|
|
||||||
|
from utilities.query_functions import EmptyGroupByJSONBAgg, OrderableJSONBAgg
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
|
||||||
|
|
||||||
@@ -23,9 +24,12 @@ class CustomFieldQueryset:
|
|||||||
|
|
||||||
class ConfigContextQuerySet(RestrictedQuerySet):
|
class ConfigContextQuerySet(RestrictedQuerySet):
|
||||||
|
|
||||||
def get_for_object(self, obj):
|
def get_for_object(self, obj, aggregate_data=False):
|
||||||
"""
|
"""
|
||||||
Return all applicable ConfigContexts for a given object. Only active ConfigContexts will be included.
|
Return all applicable ConfigContexts for a given object. Only active ConfigContexts will be included.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
aggregate_data: If True, use the JSONBAgg aggregate function to return only the list of JSON data objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# `device_role` for Device; `role` for VirtualMachine
|
# `device_role` for Device; `role` for VirtualMachine
|
||||||
@@ -45,7 +49,7 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
|||||||
else:
|
else:
|
||||||
regions = []
|
regions = []
|
||||||
|
|
||||||
return self.filter(
|
queryset = self.filter(
|
||||||
Q(regions__in=regions) | Q(regions=None),
|
Q(regions__in=regions) | Q(regions=None),
|
||||||
Q(sites=obj.site) | Q(sites=None),
|
Q(sites=obj.site) | Q(sites=None),
|
||||||
Q(roles=role) | Q(roles=None),
|
Q(roles=role) | Q(roles=None),
|
||||||
@@ -57,3 +61,72 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
|||||||
Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
|
Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).order_by('weight', 'name')
|
).order_by('weight', 'name')
|
||||||
|
|
||||||
|
if aggregate_data:
|
||||||
|
return queryset.aggregate(
|
||||||
|
config_context_data=OrderableJSONBAgg('data', ordering=['weight', 'name'])
|
||||||
|
)['config_context_data']
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||||
|
"""
|
||||||
|
QuerySet manager used by models which support ConfigContext (device and virtual machine).
|
||||||
|
|
||||||
|
Includes a method which appends an annotation of aggregated config context JSON data objects. This is
|
||||||
|
implemented as a subquery which performs all the joins necessary to filter relevant config context objects.
|
||||||
|
This offers a substantial performance gain over ConfigContextQuerySet.get_for_object() when dealing with
|
||||||
|
multiple objects.
|
||||||
|
|
||||||
|
This allows the annotation to be entirely optional.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def annotate_config_context_data(self):
|
||||||
|
"""
|
||||||
|
Attach the subquery annotation to the base queryset
|
||||||
|
"""
|
||||||
|
from extras.models import ConfigContext
|
||||||
|
return self.annotate(
|
||||||
|
config_context_data=Subquery(
|
||||||
|
ConfigContext.objects.filter(
|
||||||
|
self._get_config_context_filters()
|
||||||
|
).annotate(
|
||||||
|
_data=EmptyGroupByJSONBAgg('data', ordering=['weight', 'name'])
|
||||||
|
).values("_data")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_config_context_filters(self):
|
||||||
|
# Construct the set of Q objects for the specific object types
|
||||||
|
base_query = Q(
|
||||||
|
Q(platforms=OuterRef('platform')) | Q(platforms=None),
|
||||||
|
Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None),
|
||||||
|
Q(tenants=OuterRef('tenant')) | Q(tenants=None),
|
||||||
|
Q(tags=OuterRef('tags')) | Q(tags=None),
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.model._meta.model_name == 'device':
|
||||||
|
base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
|
||||||
|
base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)
|
||||||
|
region_field = 'site__region'
|
||||||
|
|
||||||
|
elif self.model._meta.model_name == 'virtualmachine':
|
||||||
|
base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)
|
||||||
|
base_query.add((Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None)), Q.AND)
|
||||||
|
base_query.add((Q(clusters=OuterRef('cluster')) | Q(clusters=None)), Q.AND)
|
||||||
|
base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND)
|
||||||
|
region_field = 'cluster__site__region'
|
||||||
|
|
||||||
|
base_query.add(
|
||||||
|
(Q(
|
||||||
|
regions__tree_id=OuterRef(f'{region_field}__tree_id'),
|
||||||
|
regions__level__lte=OuterRef(f'{region_field}__level'),
|
||||||
|
regions__lft__lte=OuterRef(f'{region_field}__lft'),
|
||||||
|
regions__rght__gte=OuterRef(f'{region_field}__rght'),
|
||||||
|
) | Q(regions=None)),
|
||||||
|
Q.AND
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_query
|
||||||
|
|||||||
@@ -35,10 +35,8 @@ OBJECTCHANGE_ACTION = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
OBJECTCHANGE_OBJECT = """
|
OBJECTCHANGE_OBJECT = """
|
||||||
{% if record.action != 3 and record.changed_object.get_absolute_url %}
|
{% if record.changed_object.get_absolute_url %}
|
||||||
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||||
{% elif record.action != 3 and record.related_object.get_absolute_url %}
|
|
||||||
<a href="{{ record.related_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ record.object_repr }}
|
{{ record.object_repr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ GROUP_BUTTON = '<div class="btn-group">\n' \
|
|||||||
GROUP_LINK = '<li><a href="{}"{}>{}</a></li>\n'
|
GROUP_LINK = '<li><a href="{}"{}>{}</a></li>\n'
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag(takes_context=True)
|
||||||
def custom_links(obj):
|
def custom_links(context, obj):
|
||||||
"""
|
"""
|
||||||
Render all applicable links for the given object.
|
Render all applicable links for the given object.
|
||||||
"""
|
"""
|
||||||
@@ -30,8 +30,13 @@ def custom_links(obj):
|
|||||||
if not custom_links:
|
if not custom_links:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
context = {
|
# Pass select context data when rendering the CustomLink
|
||||||
|
link_context = {
|
||||||
'obj': obj,
|
'obj': obj,
|
||||||
|
'debug': context.get('debug', False), # django.template.context_processors.debug
|
||||||
|
'request': context['request'], # django.template.context_processors.request
|
||||||
|
'user': context['user'], # django.contrib.auth.context_processors.auth
|
||||||
|
'perms': context['perms'], # django.contrib.auth.context_processors.auth
|
||||||
}
|
}
|
||||||
template_code = ''
|
template_code = ''
|
||||||
group_names = OrderedDict()
|
group_names = OrderedDict()
|
||||||
@@ -47,9 +52,9 @@ def custom_links(obj):
|
|||||||
# Add non-grouped links
|
# Add non-grouped links
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
text_rendered = render_jinja2(cl.text, context)
|
text_rendered = render_jinja2(cl.text, link_context)
|
||||||
if text_rendered:
|
if text_rendered:
|
||||||
link_rendered = render_jinja2(cl.url, context)
|
link_rendered = render_jinja2(cl.url, link_context)
|
||||||
link_target = ' target="_blank"' if cl.new_window else ''
|
link_target = ' target="_blank"' if cl.new_window else ''
|
||||||
template_code += LINK_BUTTON.format(
|
template_code += LINK_BUTTON.format(
|
||||||
link_rendered, link_target, cl.button_class, text_rendered
|
link_rendered, link_target, cl.button_class, text_rendered
|
||||||
@@ -65,10 +70,10 @@ def custom_links(obj):
|
|||||||
|
|
||||||
for cl in links:
|
for cl in links:
|
||||||
try:
|
try:
|
||||||
text_rendered = render_jinja2(cl.text, context)
|
text_rendered = render_jinja2(cl.text, link_context)
|
||||||
if text_rendered:
|
if text_rendered:
|
||||||
link_target = ' target="_blank"' if cl.new_window else ''
|
link_target = ' target="_blank"' if cl.new_window else ''
|
||||||
link_rendered = render_jinja2(cl.url, context)
|
link_rendered = render_jinja2(cl.url, link_context)
|
||||||
links_rendered.append(
|
links_rendered.append(
|
||||||
GROUP_LINK.format(link_rendered, link_target, text_rendered)
|
GROUP_LINK.format(link_rendered, link_target, text_rendered)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
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 dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Rack, Region, Site
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.filters import *
|
from extras.filters import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
from extras.models import ConfigContext, ExportTemplate, Graph, Tag
|
from extras.models import ConfigContext, ExportTemplate, Graph, ImageAttachment, Tag
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
@@ -78,6 +78,84 @@ class ExportTemplateTestCase(TestCase):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAttachmentTestCase(TestCase):
|
||||||
|
queryset = ImageAttachment.objects.all()
|
||||||
|
filterset = ImageAttachmentFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
site_ct = ContentType.objects.get(app_label='dcim', model='site')
|
||||||
|
rack_ct = ContentType.objects.get(app_label='dcim', model='rack')
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
|
image_attachments = (
|
||||||
|
ImageAttachment(
|
||||||
|
content_type=site_ct,
|
||||||
|
object_id=sites[0].pk,
|
||||||
|
name='Image Attachment 1',
|
||||||
|
image='http://example.com/image1.png',
|
||||||
|
image_height=100,
|
||||||
|
image_width=100
|
||||||
|
),
|
||||||
|
ImageAttachment(
|
||||||
|
content_type=site_ct,
|
||||||
|
object_id=sites[1].pk,
|
||||||
|
name='Image Attachment 2',
|
||||||
|
image='http://example.com/image2.png',
|
||||||
|
image_height=100,
|
||||||
|
image_width=100
|
||||||
|
),
|
||||||
|
ImageAttachment(
|
||||||
|
content_type=rack_ct,
|
||||||
|
object_id=racks[0].pk,
|
||||||
|
name='Image Attachment 3',
|
||||||
|
image='http://example.com/image3.png',
|
||||||
|
image_height=100,
|
||||||
|
image_width=100
|
||||||
|
),
|
||||||
|
ImageAttachment(
|
||||||
|
content_type=rack_ct,
|
||||||
|
object_id=racks[1].pk,
|
||||||
|
name='Image Attachment 4',
|
||||||
|
image='http://example.com/image4.png',
|
||||||
|
image_height=100,
|
||||||
|
image_width=100
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ImageAttachment.objects.bulk_create(image_attachments)
|
||||||
|
|
||||||
|
def test_id(self):
|
||||||
|
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_content_type(self):
|
||||||
|
params = {'content_type': ContentType.objects.get(app_label='dcim', model='site').pk}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_content_type_and_object_id(self):
|
||||||
|
params = {
|
||||||
|
'content_type': ContentType.objects.get(app_label='dcim', model='site').pk,
|
||||||
|
'object_id': [Site.objects.first().pk],
|
||||||
|
}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextTestCase(TestCase):
|
class ConfigContextTestCase(TestCase):
|
||||||
queryset = ConfigContext.objects.all()
|
queryset = ConfigContext.objects.all()
|
||||||
filterset = ConfigContextFilterSet
|
filterset = ConfigContextFilterSet
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
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 dcim.models import Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Site, Region
|
||||||
from extras.choices import TemplateLanguageChoices
|
from extras.choices import TemplateLanguageChoices
|
||||||
from extras.models import Graph, Tag
|
from extras.models import ConfigContext, Graph, Tag
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
class GraphTest(TestCase):
|
class GraphTest(TestCase):
|
||||||
@@ -53,3 +55,276 @@ class TagTest(TestCase):
|
|||||||
tag.save()
|
tag.save()
|
||||||
|
|
||||||
self.assertEqual(tag.slug, 'testing-unicode-台灣')
|
self.assertEqual(tag.slug, 'testing-unicode-台灣')
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextTest(TestCase):
|
||||||
|
"""
|
||||||
|
These test cases deal with the weighting, ordering, and deep merge logic of config context data.
|
||||||
|
|
||||||
|
It also ensures the various config context querysets are consistent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
|
self.devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||||
|
self.devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
|
self.region = Region.objects.create(name="Region")
|
||||||
|
self.site = Site.objects.create(name='Site-1', slug='site-1', region=self.region)
|
||||||
|
self.platform = Platform.objects.create(name="Platform")
|
||||||
|
self.tenantgroup = TenantGroup.objects.create(name="Tenant Group")
|
||||||
|
self.tenant = Tenant.objects.create(name="Tenant", group=self.tenantgroup)
|
||||||
|
self.tag = Tag.objects.create(name="Tag", slug="tag")
|
||||||
|
|
||||||
|
self.device = Device.objects.create(
|
||||||
|
name='Device 1',
|
||||||
|
device_type=self.devicetype,
|
||||||
|
device_role=self.devicerole,
|
||||||
|
site=self.site
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_higher_weight_wins(self):
|
||||||
|
|
||||||
|
context1 = ConfigContext(
|
||||||
|
name="context 1",
|
||||||
|
weight=101,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 777
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context2 = ConfigContext(
|
||||||
|
name="context 2",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 789
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigContext.objects.bulk_create([context1, context2])
|
||||||
|
|
||||||
|
expected_data = {
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 777
|
||||||
|
}
|
||||||
|
self.assertEqual(self.device.get_config_context(), expected_data)
|
||||||
|
|
||||||
|
def test_name_ordering_after_weight(self):
|
||||||
|
|
||||||
|
context1 = ConfigContext(
|
||||||
|
name="context 1",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 777
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context2 = ConfigContext(
|
||||||
|
name="context 2",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 789
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigContext.objects.bulk_create([context1, context2])
|
||||||
|
|
||||||
|
expected_data = {
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 789
|
||||||
|
}
|
||||||
|
self.assertEqual(self.device.get_config_context(), expected_data)
|
||||||
|
|
||||||
|
def test_annotation_same_as_get_for_object(self):
|
||||||
|
"""
|
||||||
|
This test incorperates features from all of the above tests cases to ensure
|
||||||
|
the annotate_config_context_data() and get_for_object() queryset methods are the same.
|
||||||
|
"""
|
||||||
|
context1 = ConfigContext(
|
||||||
|
name="context 1",
|
||||||
|
weight=101,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 777
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context2 = ConfigContext(
|
||||||
|
name="context 2",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"a": 123,
|
||||||
|
"b": 456,
|
||||||
|
"c": 789
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context3 = ConfigContext(
|
||||||
|
name="context 3",
|
||||||
|
weight=99,
|
||||||
|
data={
|
||||||
|
"d": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context4 = ConfigContext(
|
||||||
|
name="context 4",
|
||||||
|
weight=99,
|
||||||
|
data={
|
||||||
|
"d": 2
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigContext.objects.bulk_create([context1, context2, context3, context4])
|
||||||
|
|
||||||
|
annotated_queryset = Device.objects.filter(name=self.device.name).annotate_config_context_data()
|
||||||
|
self.assertEqual(self.device.get_config_context(), annotated_queryset[0].get_config_context())
|
||||||
|
|
||||||
|
def test_annotation_same_as_get_for_object_device_relations(self):
|
||||||
|
|
||||||
|
site_context = ConfigContext.objects.create(
|
||||||
|
name="site",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"site": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
site_context.sites.add(self.site)
|
||||||
|
region_context = ConfigContext.objects.create(
|
||||||
|
name="region",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"region": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
region_context.regions.add(self.region)
|
||||||
|
platform_context = ConfigContext.objects.create(
|
||||||
|
name="platform",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"platform": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
platform_context.platforms.add(self.platform)
|
||||||
|
tenant_group_context = ConfigContext.objects.create(
|
||||||
|
name="tenant group",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tenant_group": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tenant_group_context.tenant_groups.add(self.tenantgroup)
|
||||||
|
tenant_context = ConfigContext.objects.create(
|
||||||
|
name="tenant",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tenant": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tenant_context.tenants.add(self.tenant)
|
||||||
|
tag_context = ConfigContext.objects.create(
|
||||||
|
name="tag",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tag": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tag_context.tags.add(self.tag)
|
||||||
|
|
||||||
|
device = Device.objects.create(
|
||||||
|
name="Device 2",
|
||||||
|
site=self.site,
|
||||||
|
tenant=self.tenant,
|
||||||
|
platform=self.platform,
|
||||||
|
device_role=self.devicerole,
|
||||||
|
device_type=self.devicetype
|
||||||
|
)
|
||||||
|
device.tags.add(self.tag)
|
||||||
|
|
||||||
|
annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
|
||||||
|
self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
|
||||||
|
|
||||||
|
def test_annotation_same_as_get_for_object_virtualmachine_relations(self):
|
||||||
|
|
||||||
|
site_context = ConfigContext.objects.create(
|
||||||
|
name="site",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"site": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
site_context.sites.add(self.site)
|
||||||
|
region_context = ConfigContext.objects.create(
|
||||||
|
name="region",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"region": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
region_context.regions.add(self.region)
|
||||||
|
platform_context = ConfigContext.objects.create(
|
||||||
|
name="platform",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"platform": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
platform_context.platforms.add(self.platform)
|
||||||
|
tenant_group_context = ConfigContext.objects.create(
|
||||||
|
name="tenant group",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tenant_group": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tenant_group_context.tenant_groups.add(self.tenantgroup)
|
||||||
|
tenant_context = ConfigContext.objects.create(
|
||||||
|
name="tenant",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tenant": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tenant_context.tenants.add(self.tenant)
|
||||||
|
tag_context = ConfigContext.objects.create(
|
||||||
|
name="tag",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"tag": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tag_context.tags.add(self.tag)
|
||||||
|
cluster_group = ClusterGroup.objects.create(name="Cluster Group")
|
||||||
|
cluster_group_context = ConfigContext.objects.create(
|
||||||
|
name="cluster group",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"cluster_group": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cluster_group_context.cluster_groups.add(cluster_group)
|
||||||
|
cluster_type = ClusterType.objects.create(name="Cluster Type 1")
|
||||||
|
cluster = Cluster.objects.create(name="Cluster", group=cluster_group, type=cluster_type)
|
||||||
|
cluster_context = ConfigContext.objects.create(
|
||||||
|
name="cluster",
|
||||||
|
weight=100,
|
||||||
|
data={
|
||||||
|
"cluster": 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cluster_context.clusters.add(cluster)
|
||||||
|
|
||||||
|
virtual_machine = VirtualMachine.objects.create(
|
||||||
|
name="VM 1",
|
||||||
|
cluster=cluster,
|
||||||
|
tenant=self.tenant,
|
||||||
|
platform=self.platform,
|
||||||
|
role=self.devicerole
|
||||||
|
)
|
||||||
|
virtual_machine.tags.add(self.tag)
|
||||||
|
|
||||||
|
annotated_queryset = VirtualMachine.objects.filter(name=virtual_machine.name).annotate_config_context_data()
|
||||||
|
self.assertEqual(virtual_machine.get_config_context(), annotated_queryset[0].get_config_context())
|
||||||
|
|||||||
@@ -59,3 +59,21 @@ class TaggedItemTest(APITestCase):
|
|||||||
sorted([t.name for t in site.tags.all()]),
|
sorted([t.name for t in site.tags.all()]),
|
||||||
sorted(["Foo", "Bar", "New Tag"])
|
sorted(["Foo", "Bar", "New Tag"])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_clear_tagged_item(self):
|
||||||
|
site = Site.objects.create(
|
||||||
|
name='Test Site',
|
||||||
|
slug='test-site'
|
||||||
|
)
|
||||||
|
site.tags.add("Foo", "Bar", "Baz")
|
||||||
|
data = {
|
||||||
|
'tags': []
|
||||||
|
}
|
||||||
|
self.add_permissions('dcim.change_site')
|
||||||
|
url = reverse('dcim-api:site-detail', kwargs={'pk': site.pk})
|
||||||
|
|
||||||
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data['tags']), 0)
|
||||||
|
site = Site.objects.get(pk=response.data['id'])
|
||||||
|
self.assertEqual(len(site.tags.all()), 0)
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import urllib.parse
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.choices import ObjectChangeActionChoices
|
from extras.choices import ObjectChangeActionChoices
|
||||||
from extras.models import ConfigContext, ObjectChange, Tag
|
from extras.models import ConfigContext, CustomLink, ObjectChange, Tag
|
||||||
from utilities.testing import ViewTestCases, TestCase
|
from utilities.testing import ViewTestCases, TestCase
|
||||||
|
|
||||||
|
|
||||||
@@ -124,3 +126,24 @@ class ObjectChangeTestCase(TestCase):
|
|||||||
objectchange = ObjectChange.objects.first()
|
objectchange = ObjectChange.objects.first()
|
||||||
response = self.client.get(objectchange.get_absolute_url())
|
response = self.client.get(objectchange.get_absolute_url())
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkTest(TestCase):
|
||||||
|
user_permissions = ['dcim.view_site']
|
||||||
|
|
||||||
|
def test_view_object_with_custom_link(self):
|
||||||
|
customlink = CustomLink(
|
||||||
|
content_type=ContentType.objects.get_for_model(Site),
|
||||||
|
name='Test',
|
||||||
|
text='FOO {{ obj.name }} BAR',
|
||||||
|
url='http://example.com/?site={{ obj.slug }}',
|
||||||
|
new_window=False
|
||||||
|
)
|
||||||
|
customlink.save()
|
||||||
|
|
||||||
|
site = Site(name='Test Site', slug='test-site')
|
||||||
|
site.save()
|
||||||
|
|
||||||
|
response = self.client.get(site.get_absolute_url(), follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(f'FOO {site.name} BAR', str(response.content))
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Count, Prefetch, Q
|
from django.db.models import Count, Prefetch, Q
|
||||||
@@ -13,7 +12,7 @@ from rq import Worker
|
|||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
from utilities.utils import copy_safe_request, shallow_compare_dict
|
from utilities.utils import copy_safe_request, shallow_compare_dict
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
@@ -258,7 +257,7 @@ class ObjectChangeLogView(View):
|
|||||||
# Apply the request context
|
# Apply the request context
|
||||||
paginate = {
|
paginate = {
|
||||||
'paginator_class': EnhancedPaginator,
|
'paginator_class': EnhancedPaginator,
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
'per_page': get_paginate_count(request)
|
||||||
}
|
}
|
||||||
RequestConfig(request, paginate).configure(objectchanges_table)
|
RequestConfig(request, paginate).configure(objectchanges_table)
|
||||||
|
|
||||||
@@ -315,7 +314,7 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
|
|||||||
Retrieve all of the available reports from disk and the recorded JobResult (if any) for each.
|
Retrieve all of the available reports from disk and the recorded JobResult (if any) for each.
|
||||||
"""
|
"""
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return 'extras.view_reportresult'
|
return 'extras.view_report'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
||||||
@@ -347,7 +346,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
|
|||||||
Display a single Report and its associated JobResult (if any).
|
Display a single Report and its associated JobResult (if any).
|
||||||
"""
|
"""
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return 'extras.view_reportresult'
|
return 'extras.view_report'
|
||||||
|
|
||||||
def get(self, request, module, name):
|
def get(self, request, module, name):
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,8 @@ class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
|||||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
||||||
assigned_object_type = ContentTypeField(
|
assigned_object_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
|
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
|
||||||
required=False
|
required=False,
|
||||||
|
allow_null=True
|
||||||
)
|
)
|
||||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||||
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
return super().get_serializer_class()
|
return super().get_serializer_class()
|
||||||
|
|
||||||
@swagger_auto_schema(method='get', responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
@swagger_auto_schema(method='get', responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
||||||
@swagger_auto_schema(method='post', responses={201: serializers.AvailablePrefixSerializer(many=True)})
|
@swagger_auto_schema(method='post', responses={201: serializers.PrefixSerializer(many=False)})
|
||||||
@action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
|
@action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
|
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
|
||||||
def available_prefixes(self, request, pk=None):
|
def available_prefixes(self, request, pk=None):
|
||||||
@@ -247,7 +247,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
class IPAddressViewSet(CustomFieldModelViewSet):
|
class IPAddressViewSet(CustomFieldModelViewSet):
|
||||||
queryset = IPAddress.objects.prefetch_related(
|
queryset = IPAddress.objects.prefetch_related(
|
||||||
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags',
|
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.IPAddressSerializer
|
serializer_class = serializers.IPAddressSerializer
|
||||||
filterset_class = filters.IPAddressFilterSet
|
filterset_class = filters.IPAddressFilterSet
|
||||||
|
|||||||
@@ -641,11 +641,11 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
self.initial['primary_for_parent'] = True
|
self.initial['primary_for_parent'] = True
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Cannot select both a device interface and a VM interface
|
# Cannot select both a device interface and a VM interface
|
||||||
if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'):
|
if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'):
|
||||||
raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface")
|
raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface")
|
||||||
|
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||||
|
|
||||||
# Primary IP assignment is only available if an interface has been assigned.
|
# Primary IP assignment is only available if an interface has been assigned.
|
||||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||||
@@ -655,26 +655,21 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
# Set assigned object
|
|
||||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
|
||||||
if interface:
|
|
||||||
self.instance.assigned_object = interface
|
|
||||||
|
|
||||||
ipaddress = super().save(*args, **kwargs)
|
ipaddress = super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
|
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
|
||||||
|
interface = self.instance.assigned_object
|
||||||
if interface and self.cleaned_data['primary_for_parent']:
|
if interface and self.cleaned_data['primary_for_parent']:
|
||||||
if ipaddress.address.version == 4:
|
if ipaddress.address.version == 4:
|
||||||
interface.parent.primary_ip4 = ipaddress
|
interface.parent.primary_ip4 = ipaddress
|
||||||
else:
|
else:
|
||||||
interface.primary_ip6 = ipaddress
|
interface.parent.primary_ip6 = ipaddress
|
||||||
interface.parent.save()
|
interface.parent.save()
|
||||||
elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
|
elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
|
||||||
interface.parent.primary_ip4 = None
|
interface.parent.primary_ip4 = None
|
||||||
interface.parent.save()
|
interface.parent.save()
|
||||||
elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
|
elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
|
||||||
interface.parent.primary_ip4 = None
|
interface.parent.primary_ip6 = None
|
||||||
interface.parent.save()
|
interface.parent.save()
|
||||||
|
|
||||||
return ipaddress
|
return ipaddress
|
||||||
|
|||||||
@@ -669,6 +669,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
'reserved': 'info',
|
'reserved': 'info',
|
||||||
'deprecated': 'danger',
|
'deprecated': 'danger',
|
||||||
'dhcp': 'success',
|
'dhcp': 'success',
|
||||||
|
'slaac': 'success',
|
||||||
}
|
}
|
||||||
|
|
||||||
ROLE_CLASS_MAP = {
|
ROLE_CLASS_MAP = {
|
||||||
@@ -725,30 +726,24 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Check for primary IP assignment that doesn't match the assigned device/VM
|
# Check for primary IP assignment that doesn't match the assigned device/VM
|
||||||
if self.pk and type(self.assigned_object) is Interface:
|
if self.pk:
|
||||||
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||||
if device:
|
if device:
|
||||||
if self.assigned_object is None:
|
if getattr(self.assigned_object, 'device', None) != device:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'interface': f"IP address is primary for device {device} but not assigned to an interface"
|
'interface': f"IP address is primary for device {device} but not assigned to it!"
|
||||||
})
|
})
|
||||||
elif self.assigned_object.device != device:
|
|
||||||
raise ValidationError({
|
|
||||||
'interface': f"IP address is primary for device {device} but assigned to "
|
|
||||||
f"{self.assigned_object.device} ({self.assigned_object})"
|
|
||||||
})
|
|
||||||
elif self.pk and type(self.assigned_object) is VMInterface:
|
|
||||||
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||||
if vm:
|
if vm:
|
||||||
if self.assigned_object is None:
|
if getattr(self.assigned_object, 'virtual_machine', None) != vm:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to an "
|
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to it!"
|
||||||
f"interface"
|
|
||||||
})
|
})
|
||||||
elif self.interface.virtual_machine != vm:
|
|
||||||
|
# Validate IP status selection
|
||||||
|
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'vminterface': f"IP address is primary for virtual machine {vm} but assigned to "
|
'status': "Only IPv6 addresses can be assigned SLAAC status"
|
||||||
f"{self.assigned_object.virtual_machine} ({self.assigned_object})"
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@@ -990,13 +985,20 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
|||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return self.STATUS_CLASS_MAP[self.status]
|
return self.STATUS_CLASS_MAP[self.status]
|
||||||
|
|
||||||
def get_members(self):
|
def get_interfaces(self):
|
||||||
# Return all interfaces assigned to this VLAN
|
# Return all device interfaces assigned to this VLAN
|
||||||
return Interface.objects.filter(
|
return Interface.objects.filter(
|
||||||
Q(untagged_vlan_id=self.pk) |
|
Q(untagged_vlan_id=self.pk) |
|
||||||
Q(tagged_vlans=self.pk)
|
Q(tagged_vlans=self.pk)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
def get_vminterfaces(self):
|
||||||
|
# Return all VM interfaces assigned to this VLAN
|
||||||
|
return VMInterface.objects.filter(
|
||||||
|
Q(untagged_vlan_id=self.pk) |
|
||||||
|
Q(tagged_vlans=self.pk)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||||
class Service(ChangeLoggedModel, CustomFieldModel):
|
class Service(ChangeLoggedModel, CustomFieldModel):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django_tables2.utils import Accessor
|
|||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from tenancy.tables import COL_TENANT
|
from tenancy.tables import COL_TENANT
|
||||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, TagColumn, ToggleColumn
|
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, TagColumn, ToggleColumn
|
||||||
|
from virtualization.models import VMInterface
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
RIR_UTILIZATION = """
|
RIR_UTILIZATION = """
|
||||||
@@ -67,11 +68,7 @@ IPADDRESS_LINK = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
IPADDRESS_ASSIGN_LINK = """
|
IPADDRESS_ASSIGN_LINK = """
|
||||||
{% if request.GET %}
|
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?{% if request.GET.interface %}interface={{ request.GET.interface }}{% elif request.GET.vminterface %}vminterface={{ request.GET.vminterface }}{% endif %}&return_url={{ request.GET.return_url }}">{{ record }}</a>
|
||||||
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ record.interface.pk }}&return_url={{ request.path }}">{{ record }}</a>
|
|
||||||
{% endif %}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VRF_LINK = """
|
VRF_LINK = """
|
||||||
@@ -103,7 +100,7 @@ VLAN_LINK = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
VLAN_PREFIXES = """
|
VLAN_PREFIXES = """
|
||||||
{% for prefix in record.prefixes.unrestricted %}
|
{% for prefix in record.prefixes.all %}
|
||||||
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
—
|
—
|
||||||
@@ -128,9 +125,11 @@ VLANGROUP_ADD_VLAN = """
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VLAN_MEMBER_UNTAGGED = """
|
VLAN_MEMBER_TAGGED = """
|
||||||
{% if record.untagged_vlan_id == vlan.pk %}
|
{% if record.untagged_vlan_id == vlan.pk %}
|
||||||
<i class="glyphicon glyphicon-ok">
|
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -387,15 +386,23 @@ class IPAddressTable(BaseTable):
|
|||||||
tenant = tables.TemplateColumn(
|
tenant = tables.TemplateColumn(
|
||||||
template_code=TENANT_LINK
|
template_code=TENANT_LINK
|
||||||
)
|
)
|
||||||
assigned = tables.BooleanColumn(
|
assigned_object = tables.Column(
|
||||||
accessor='assigned_object_id',
|
linkify=True,
|
||||||
verbose_name='Assigned'
|
orderable=False,
|
||||||
|
verbose_name='Interface'
|
||||||
|
)
|
||||||
|
assigned_object_parent = tables.Column(
|
||||||
|
accessor='assigned_object__parent',
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Interface Parent'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'assigned_object_parent', 'dns_name',
|
||||||
|
'description',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||||
@@ -411,6 +418,10 @@ class IPAddressDetailTable(IPAddressTable):
|
|||||||
tenant = tables.TemplateColumn(
|
tenant = tables.TemplateColumn(
|
||||||
template_code=COL_TENANT
|
template_code=COL_TENANT
|
||||||
)
|
)
|
||||||
|
assigned = BooleanColumn(
|
||||||
|
accessor='assigned_object_id',
|
||||||
|
verbose_name='Assigned'
|
||||||
|
)
|
||||||
tags = TagColumn(
|
tags = TagColumn(
|
||||||
url_name='ipam:ipaddress_list'
|
url_name='ipam:ipaddress_list'
|
||||||
)
|
)
|
||||||
@@ -545,15 +556,15 @@ class VLANDetailTable(VLANTable):
|
|||||||
default_columns = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
|
default_columns = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||||
|
|
||||||
|
|
||||||
class VLANMemberTable(BaseTable):
|
class VLANMembersTable(BaseTable):
|
||||||
parent = tables.LinkColumn(
|
"""
|
||||||
order_by=['device', 'virtual_machine']
|
Base table for Interface and VMInterface assignments
|
||||||
)
|
"""
|
||||||
name = tables.LinkColumn(
|
name = tables.LinkColumn(
|
||||||
verbose_name='Interface'
|
verbose_name='Interface'
|
||||||
)
|
)
|
||||||
untagged = tables.TemplateColumn(
|
tagged = tables.TemplateColumn(
|
||||||
template_code=VLAN_MEMBER_UNTAGGED,
|
template_code=VLAN_MEMBER_TAGGED,
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
actions = tables.TemplateColumn(
|
actions = tables.TemplateColumn(
|
||||||
@@ -562,9 +573,21 @@ class VLANMemberTable(BaseTable):
|
|||||||
verbose_name=''
|
verbose_name=''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VLANDevicesTable(VLANMembersTable):
|
||||||
|
device = tables.LinkColumn()
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ('parent', 'name', 'untagged', 'actions')
|
fields = ('device', 'name', 'tagged', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
class VLANVirtualMachinesTable(VLANMembersTable):
|
||||||
|
virtual_machine = tables.LinkColumn()
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = VMInterface
|
||||||
|
fields = ('virtual_machine', 'name', 'tagged', 'actions')
|
||||||
|
|
||||||
|
|
||||||
class InterfaceVLANTable(BaseTable):
|
class InterfaceVLANTable(BaseTable):
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ urlpatterns = [
|
|||||||
path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||||
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||||
path('vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
|
path('vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
|
||||||
path('vlans/<int:pk>/members/', views.VLANMembersView.as_view(), name='vlan_members'),
|
path('vlans/<int:pk>/interfaces/', views.VLANInterfacesView.as_view(), name='vlan_interfaces'),
|
||||||
|
path('vlans/<int:pk>/vm-interfaces/', views.VLANVMInterfacesView.as_view(), name='vlan_vminterfaces'),
|
||||||
path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
|
path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||||
path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
||||||
path('vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
path('vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||||||
from django_tables2 import RequestConfig
|
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, get_paginate_count
|
||||||
from utilities.utils import get_subquery
|
from utilities.utils import get_subquery
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
|
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
|
||||||
@@ -233,7 +233,7 @@ class AggregateView(ObjectView):
|
|||||||
|
|
||||||
paginate = {
|
paginate = {
|
||||||
'paginator_class': EnhancedPaginator,
|
'paginator_class': EnhancedPaginator,
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
'per_page': get_paginate_count(request)
|
||||||
}
|
}
|
||||||
RequestConfig(request, paginate).configure(prefix_table)
|
RequestConfig(request, paginate).configure(prefix_table)
|
||||||
|
|
||||||
@@ -391,7 +391,7 @@ class PrefixPrefixesView(ObjectView):
|
|||||||
|
|
||||||
paginate = {
|
paginate = {
|
||||||
'paginator_class': EnhancedPaginator,
|
'paginator_class': EnhancedPaginator,
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
'per_page': get_paginate_count(request)
|
||||||
}
|
}
|
||||||
RequestConfig(request, paginate).configure(prefix_table)
|
RequestConfig(request, paginate).configure(prefix_table)
|
||||||
|
|
||||||
@@ -435,7 +435,7 @@ class PrefixIPAddressesView(ObjectView):
|
|||||||
|
|
||||||
paginate = {
|
paginate = {
|
||||||
'paginator_class': EnhancedPaginator,
|
'paginator_class': EnhancedPaginator,
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
'per_page': get_paginate_count(request)
|
||||||
}
|
}
|
||||||
RequestConfig(request, paginate).configure(ip_table)
|
RequestConfig(request, paginate).configure(ip_table)
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ class PrefixBulkDeleteView(BulkDeleteView):
|
|||||||
|
|
||||||
class IPAddressListView(ObjectListView):
|
class IPAddressListView(ObjectListView):
|
||||||
queryset = IPAddress.objects.prefetch_related(
|
queryset = IPAddress.objects.prefetch_related(
|
||||||
'vrf__tenant', 'tenant', 'nat_inside'
|
'vrf__tenant', 'tenant', 'nat_inside', 'assigned_object'
|
||||||
)
|
)
|
||||||
filterset = filters.IPAddressFilterSet
|
filterset = filters.IPAddressFilterSet
|
||||||
filterset_form = forms.IPAddressFilterForm
|
filterset_form = forms.IPAddressFilterForm
|
||||||
@@ -527,7 +527,8 @@ class IPAddressView(ObjectView):
|
|||||||
# Exclude anycast IPs if this IP is anycast
|
# Exclude anycast IPs if this IP is anycast
|
||||||
if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
|
if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
|
||||||
duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST)
|
duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST)
|
||||||
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
|
# Limit to a maximum of 10 duplicates displayed here
|
||||||
|
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
|
||||||
|
|
||||||
# Related IP table
|
# Related IP table
|
||||||
related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
|
related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
|
||||||
@@ -539,7 +540,7 @@ class IPAddressView(ObjectView):
|
|||||||
|
|
||||||
paginate = {
|
paginate = {
|
||||||
'paginator_class': EnhancedPaginator,
|
'paginator_class': EnhancedPaginator,
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
'per_page': get_paginate_count(request)
|
||||||
}
|
}
|
||||||
RequestConfig(request, paginate).configure(related_ips_table)
|
RequestConfig(request, paginate).configure(related_ips_table)
|
||||||
|
|
||||||
@@ -547,6 +548,7 @@ class IPAddressView(ObjectView):
|
|||||||
'ipaddress': ipaddress,
|
'ipaddress': ipaddress,
|
||||||
'parent_prefixes_table': parent_prefixes_table,
|
'parent_prefixes_table': parent_prefixes_table,
|
||||||
'duplicate_ips_table': duplicate_ips_table,
|
'duplicate_ips_table': duplicate_ips_table,
|
||||||
|
'more_duplicate_ips': duplicate_ips.count() > 10,
|
||||||
'related_ips_table': related_ips_table,
|
'related_ips_table': related_ips_table,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -582,7 +584,7 @@ class IPAddressAssignView(ObjectView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
|
||||||
# Redirect user if an interface has not been provided
|
# Redirect user if an interface has not been provided
|
||||||
if 'interface' not in request.GET:
|
if 'interface' not in request.GET and 'vminterface' not in request.GET:
|
||||||
return redirect('ipam:ipaddress_add')
|
return redirect('ipam:ipaddress_add')
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
@@ -609,7 +611,7 @@ class IPAddressAssignView(ObjectView):
|
|||||||
return render(request, 'ipam/ipaddress_assign.html', {
|
return render(request, 'ipam/ipaddress_assign.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
'table': table,
|
'table': table,
|
||||||
'return_url': request.GET.get('return_url', ''),
|
'return_url': request.GET.get('return_url'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -699,7 +701,7 @@ class VLANGroupVLANsView(ObjectView):
|
|||||||
|
|
||||||
paginate = {
|
paginate = {
|
||||||
'paginator_class': EnhancedPaginator,
|
'paginator_class': EnhancedPaginator,
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
'per_page': get_paginate_count(request),
|
||||||
}
|
}
|
||||||
RequestConfig(request, paginate).configure(vlan_table)
|
RequestConfig(request, paginate).configure(vlan_table)
|
||||||
|
|
||||||
@@ -713,6 +715,7 @@ class VLANGroupVLANsView(ObjectView):
|
|||||||
return render(request, 'ipam/vlangroup_vlans.html', {
|
return render(request, 'ipam/vlangroup_vlans.html', {
|
||||||
'vlan_group': vlan_group,
|
'vlan_group': vlan_group,
|
||||||
'first_available_vlan': vlan_group.get_next_available_vid(),
|
'first_available_vlan': vlan_group.get_next_available_vid(),
|
||||||
|
'bulk_querystring': 'group_id={}'.format(vlan_group.pk),
|
||||||
'vlan_table': vlan_table,
|
'vlan_table': vlan_table,
|
||||||
'permissions': permissions,
|
'permissions': permissions,
|
||||||
})
|
})
|
||||||
@@ -749,26 +752,45 @@ class VLANView(ObjectView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class VLANMembersView(ObjectView):
|
class VLANInterfacesView(ObjectView):
|
||||||
queryset = VLAN.objects.all()
|
queryset = VLAN.objects.all()
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
|
|
||||||
vlan = get_object_or_404(self.queryset, pk=pk)
|
vlan = get_object_or_404(self.queryset, pk=pk)
|
||||||
members = vlan.get_members().restrict(request.user, 'view').prefetch_related('device', 'virtual_machine')
|
interfaces = vlan.get_interfaces().prefetch_related('device')
|
||||||
|
members_table = tables.VLANDevicesTable(interfaces)
|
||||||
members_table = tables.VLANMemberTable(members)
|
|
||||||
|
|
||||||
paginate = {
|
paginate = {
|
||||||
'paginator_class': EnhancedPaginator,
|
'paginator_class': EnhancedPaginator,
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
'per_page': get_paginate_count(request)
|
||||||
}
|
}
|
||||||
RequestConfig(request, paginate).configure(members_table)
|
RequestConfig(request, paginate).configure(members_table)
|
||||||
|
|
||||||
return render(request, 'ipam/vlan_members.html', {
|
return render(request, 'ipam/vlan_interfaces.html', {
|
||||||
'vlan': vlan,
|
'vlan': vlan,
|
||||||
'members_table': members_table,
|
'members_table': members_table,
|
||||||
'active_tab': 'members',
|
'active_tab': 'interfaces',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class VLANVMInterfacesView(ObjectView):
|
||||||
|
queryset = VLAN.objects.all()
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
vlan = get_object_or_404(self.queryset, pk=pk)
|
||||||
|
interfaces = vlan.get_vminterfaces().prefetch_related('virtual_machine')
|
||||||
|
members_table = tables.VLANVirtualMachinesTable(interfaces)
|
||||||
|
|
||||||
|
paginate = {
|
||||||
|
'paginator_class': EnhancedPaginator,
|
||||||
|
'per_page': get_paginate_count(request)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(members_table)
|
||||||
|
|
||||||
|
return render(request, 'ipam/vlan_vminterfaces.html', {
|
||||||
|
'vlan': vlan,
|
||||||
|
'members_table': members_table,
|
||||||
|
'active_tab': 'vminterfaces',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -172,9 +172,4 @@ class LDAPBackend:
|
|||||||
if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False):
|
if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False):
|
||||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||||
|
|
||||||
# Enable logging for django_auth_ldap
|
|
||||||
ldap_logger = logging.getLogger('django_auth_ldap')
|
|
||||||
ldap_logger.addHandler(logging.StreamHandler())
|
|
||||||
ldap_logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ REDIS = {
|
|||||||
# 'SENTINEL_SERVICE': 'netbox',
|
# 'SENTINEL_SERVICE': 'netbox',
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 0,
|
'DATABASE': 0,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
},
|
},
|
||||||
'caching': {
|
'caching': {
|
||||||
@@ -44,7 +43,6 @@ REDIS = {
|
|||||||
# 'SENTINEL_SERVICE': 'netbox',
|
# 'SENTINEL_SERVICE': 'netbox',
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 1,
|
'DATABASE': 1,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,6 +230,9 @@ RELEASE_CHECK_URL = None
|
|||||||
# this setting is derived from the installed location.
|
# this setting is derived from the installed location.
|
||||||
# REPORTS_ROOT = '/opt/netbox/netbox/reports'
|
# REPORTS_ROOT = '/opt/netbox/netbox/reports'
|
||||||
|
|
||||||
|
# Maximum execution time for background tasks, in seconds.
|
||||||
|
RQ_DEFAULT_TIMEOUT = 300
|
||||||
|
|
||||||
# The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of
|
# The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of
|
||||||
# this setting is derived from the installed location.
|
# this setting is derived from the installed location.
|
||||||
# SCRIPTS_ROOT = '/opt/netbox/netbox/scripts'
|
# SCRIPTS_ROOT = '/opt/netbox/netbox/scripts'
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ REDIS = {
|
|||||||
'PORT': 6379,
|
'PORT': 6379,
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 0,
|
'DATABASE': 0,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
},
|
},
|
||||||
'caching': {
|
'caching': {
|
||||||
@@ -32,7 +31,6 @@ REDIS = {
|
|||||||
'PORT': 6379,
|
'PORT': 6379,
|
||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 1,
|
'DATABASE': 1,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
|
||||||
'SSL': False,
|
'SSL': False,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.9.1'
|
VERSION = '2.9.8'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@@ -110,6 +110,7 @@ REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_U
|
|||||||
RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
|
RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
|
||||||
RELEASE_CHECK_TIMEOUT = getattr(configuration, 'RELEASE_CHECK_TIMEOUT', 24 * 3600)
|
RELEASE_CHECK_TIMEOUT = getattr(configuration, 'RELEASE_CHECK_TIMEOUT', 24 * 3600)
|
||||||
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
||||||
|
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
||||||
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
||||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||||
@@ -220,10 +221,13 @@ TASKS_REDIS_USING_SENTINEL = all([
|
|||||||
len(TASKS_REDIS_SENTINELS) > 0
|
len(TASKS_REDIS_SENTINELS) > 0
|
||||||
])
|
])
|
||||||
TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default')
|
TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default')
|
||||||
|
TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
|
||||||
TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
|
TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
|
||||||
TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
|
TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
|
||||||
TASKS_REDIS_DEFAULT_TIMEOUT = TASKS_REDIS.get('DEFAULT_TIMEOUT', 300)
|
|
||||||
TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
|
TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
|
||||||
|
# TODO: Remove in v2.10 (see #5171)
|
||||||
|
if 'DEFAULT_TIMEOUT' in TASKS_REDIS:
|
||||||
|
warnings.warn('DEFAULT_TIMEOUT is no longer supported under REDIS configuration. Set RQ_DEFAULT_TIMEOUT instead.')
|
||||||
|
|
||||||
# Caching
|
# Caching
|
||||||
if 'caching' not in REDIS:
|
if 'caching' not in REDIS:
|
||||||
@@ -241,7 +245,6 @@ CACHING_REDIS_USING_SENTINEL = all([
|
|||||||
CACHING_REDIS_SENTINEL_SERVICE = CACHING_REDIS.get('SENTINEL_SERVICE', 'default')
|
CACHING_REDIS_SENTINEL_SERVICE = CACHING_REDIS.get('SENTINEL_SERVICE', 'default')
|
||||||
CACHING_REDIS_PASSWORD = CACHING_REDIS.get('PASSWORD', '')
|
CACHING_REDIS_PASSWORD = CACHING_REDIS.get('PASSWORD', '')
|
||||||
CACHING_REDIS_DATABASE = CACHING_REDIS.get('DATABASE', 0)
|
CACHING_REDIS_DATABASE = CACHING_REDIS.get('DATABASE', 0)
|
||||||
CACHING_REDIS_DEFAULT_TIMEOUT = CACHING_REDIS.get('DEFAULT_TIMEOUT', 300)
|
|
||||||
CACHING_REDIS_SSL = CACHING_REDIS.get('SSL', False)
|
CACHING_REDIS_SSL = CACHING_REDIS.get('SSL', False)
|
||||||
|
|
||||||
|
|
||||||
@@ -549,7 +552,7 @@ if TASKS_REDIS_USING_SENTINEL:
|
|||||||
'PASSWORD': TASKS_REDIS_PASSWORD,
|
'PASSWORD': TASKS_REDIS_PASSWORD,
|
||||||
'SOCKET_TIMEOUT': None,
|
'SOCKET_TIMEOUT': None,
|
||||||
'CONNECTION_KWARGS': {
|
'CONNECTION_KWARGS': {
|
||||||
'socket_connect_timeout': TASKS_REDIS_DEFAULT_TIMEOUT
|
'socket_connect_timeout': TASKS_REDIS_SENTINEL_TIMEOUT
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
@@ -558,8 +561,8 @@ else:
|
|||||||
'PORT': TASKS_REDIS_PORT,
|
'PORT': TASKS_REDIS_PORT,
|
||||||
'DB': TASKS_REDIS_DATABASE,
|
'DB': TASKS_REDIS_DATABASE,
|
||||||
'PASSWORD': TASKS_REDIS_PASSWORD,
|
'PASSWORD': TASKS_REDIS_PASSWORD,
|
||||||
'DEFAULT_TIMEOUT': TASKS_REDIS_DEFAULT_TIMEOUT,
|
|
||||||
'SSL': TASKS_REDIS_SSL,
|
'SSL': TASKS_REDIS_SSL,
|
||||||
|
'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
|
||||||
}
|
}
|
||||||
|
|
||||||
RQ_QUEUES = {
|
RQ_QUEUES = {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
Python version: {{ python_version }}
|
Python version: {{ python_version }}
|
||||||
NetBox version: {{ netbox_version }}</pre>
|
NetBox version: {{ netbox_version }}</pre>
|
||||||
<p>
|
<p>
|
||||||
If further assistance is required, please post to the <a href="https://groups.google.com/forum/#!forum/netbox-discuss">NetBox mailing list</a>.
|
If further assistance is required, please post to the <a href="https://groups.google.com/g/netbox-discuss">NetBox mailing list</a>.
|
||||||
</p>
|
</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>
|
||||||
|
|||||||
@@ -11,11 +11,8 @@
|
|||||||
<div class="row noprint">
|
<div class="row noprint">
|
||||||
<div class="col-sm-8 col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
|
<li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||||
{% if device.rack %}
|
<li><a href="{% url 'dcim:device_list' %}?site={{ device.site.slug }}">{{ device.site }}</a></li>
|
||||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.site.slug }}">Racks</a></li>
|
|
||||||
<li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li>
|
|
||||||
{% 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 }}</li>
|
<li>{{ device.parent_bay }}</li>
|
||||||
@@ -51,28 +48,28 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if perms.dcim.add_consoleport %}
|
{% if perms.dcim.add_consoleport %}
|
||||||
<li><a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Ports</a></li>
|
<li><a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_consoleserverport %}
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
<li><a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Server Ports</a></li>
|
<li><a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_powerport %}
|
{% if perms.dcim.add_powerport %}
|
||||||
<li><a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Power Ports</a></li>
|
<li><a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlet %}
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
<li><a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Power Outlets</a></li>
|
<li><a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.dcim.add_interface %}
|
||||||
<li><a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Interfaces</a></li>
|
<li><a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_frontport %}
|
{% if perms.dcim.add_frontport %}
|
||||||
<li><a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Front Ports</a></li>
|
<li><a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_rearport %}
|
{% if perms.dcim.add_rearport %}
|
||||||
<li><a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Rear Ports</a></li>
|
<li><a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_devicebay %}
|
{% if perms.dcim.add_devicebay %}
|
||||||
<li><a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Device Bays</a></li>
|
<li><a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_devicebays">Device Bays</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_inventoryitem %}
|
{% if perms.dcim.add_inventoryitem %}
|
||||||
<li><a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}">Inventory Items</a></li>
|
<li><a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}">Inventory Items</a></li>
|
||||||
@@ -101,7 +98,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">
|
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">
|
||||||
Inventory <span class="badge">{{ device.inventoryitems.unrestricted.count }}</span>
|
Inventory <span class="badge">{{ device.inventoryitems.count }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.dcim.napalm_read_device %}
|
{% if perms.dcim.napalm_read_device %}
|
||||||
@@ -151,8 +148,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 }}</a>
|
{% for group in device.rack.group.get_ancestors %}
|
||||||
<i class="fa fa-angle-right"></i>
|
<a href="{{ group.get_absolute_url }}">{{ group }}</a> <i class="fa fa-caret-right"></i>
|
||||||
|
{% endfor %}
|
||||||
|
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group }}</a> <i class="fa fa-caret-right"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a>
|
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -327,7 +326,7 @@
|
|||||||
{% plugin_left_page device %}
|
{% plugin_left_page device %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% if power_ports and poweroutlets %}
|
{% if powerports and poweroutlets %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Power Utilization</strong>
|
<strong>Power Utilization</strong>
|
||||||
@@ -340,7 +339,7 @@
|
|||||||
<th>Available</th>
|
<th>Available</th>
|
||||||
<th>Utilization</th>
|
<th>Utilization</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for pp in power_ports %}
|
{% for pp in powerports %}
|
||||||
{% with utilization=pp.get_power_draw powerfeed=pp.connected_endpoint %}
|
{% with utilization=pp.get_power_draw powerfeed=pp.connected_endpoint %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ pp }}</td>
|
<td>{{ pp }}</td>
|
||||||
@@ -538,26 +537,26 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if interfaces and perms.dcim.change_interface %}
|
{% if interfaces and perms.dcim.change_interface %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces and perms.dcim.change_interface %}
|
{% if interfaces and perms.dcim.change_interface %}
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces and perms.dcim.delete_interface %}
|
{% if interfaces and perms.dcim.delete_interface %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.dcim.add_interface %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_interfaces" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -598,24 +597,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if frontports and perms.dcim.change_frontport %}
|
{% if frontports and perms.dcim.change_frontport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if frontports and perms.dcim.delete_frontport %}
|
{% if frontports and perms.dcim.delete_frontport %}
|
||||||
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_frontport %}
|
{% if perms.dcim.add_frontport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_frontports" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -655,24 +654,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if rearports and perms.dcim.change_rearport %}
|
{% if rearports and perms.dcim.change_rearport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if rearports and perms.dcim.delete_rearport %}
|
{% if rearports and perms.dcim.delete_rearport %}
|
||||||
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_rearport %}
|
{% if perms.dcim.add_rearport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_rearports" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -709,24 +708,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if consoleports and perms.dcim.change_consoleport %}
|
{% if consoleports and perms.dcim.change_consoleport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if consoleports and perms.dcim.delete_consoleport %}
|
{% if consoleports and perms.dcim.delete_consoleport %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_consoleport %}
|
{% if perms.dcim.add_consoleport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
<a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleports" class="btn btn-xs btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -764,24 +763,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if consoleserverports and perms.dcim.change_consoleport %}
|
{% if consoleserverports and perms.dcim.change_consoleport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if consoleserverports and perms.dcim.delete_consoleserverport %}
|
{% if consoleserverports and perms.dcim.delete_consoleserverport %}
|
||||||
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_consoleserverport %}
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_consoleserverports" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -819,24 +818,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if powerports and perms.dcim.change_powerport %}
|
{% if powerports and perms.dcim.change_powerport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if powerports and perms.dcim.delete_powerport %}
|
{% if powerports and perms.dcim.delete_powerport %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_powerport %}
|
{% if perms.dcim.add_powerport %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
<a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_powerports" class="btn btn-xs btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -875,24 +874,24 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if poweroutlets and perms.dcim.change_powerport %}
|
{% if poweroutlets and perms.dcim.change_powerport %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if poweroutlets and perms.dcim.delete_poweroutlet %}
|
{% if poweroutlets and perms.dcim.delete_poweroutlet %}
|
||||||
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlet %}
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_poweroutlets" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -934,18 +933,18 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if devicebays and perms.dcim.change_devicebay %}
|
{% if devicebays and perms.dcim.change_devicebay %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}%23tab_devicebays" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if devicebays and perms.dcim.delete_devicebay %}
|
{% if devicebays and perms.dcim.delete_devicebay %}
|
||||||
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}%23tab_devicebays" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_devicebay %}
|
{% if perms.dcim.add_devicebay %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}%23tab_devicebays" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -964,6 +963,16 @@
|
|||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
// Redirect user to appropriate components tab if specified
|
||||||
|
var hash = document.location.hash;
|
||||||
|
var prefix = "tab_";
|
||||||
|
if (hash) {
|
||||||
|
$('.nav-tabs a[href="'+hash.replace(prefix,"")+'"]').tab('show');
|
||||||
|
}
|
||||||
|
$('.nav-tabs a').on('shown.bs.tab', function (e) {
|
||||||
|
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||||
|
});
|
||||||
|
|
||||||
function toggleConnection(elem) {
|
function toggleConnection(elem) {
|
||||||
var url = netbox_api_path + "dcim/cables/" + elem.attr('data') + "/";
|
var url = netbox_api_path + "dcim/cables/" + elem.attr('data') + "/";
|
||||||
if (elem.hasClass('connected')) {
|
if (elem.hasClass('connected')) {
|
||||||
|
|||||||
16
netbox/templates/dcim/device_component_edit.html
Normal file
16
netbox/templates/dcim/device_component_edit.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends 'utilities/obj_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block form_fields %}
|
||||||
|
{% if form.instance.device %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required" for="id_device">Device</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">
|
||||||
|
<a href="{{ form.instance.device.get_absolute_url }}">{{ form.instance.device }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% render_form form %}
|
||||||
|
{% endblock %}
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_field form.region %}
|
{% render_field form.region %}
|
||||||
{% render_field form.site %}
|
{% render_field form.site %}
|
||||||
|
{% render_field form.rack_group %}
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
{% if obj.device_type.is_child_device and obj.parent_bay %}
|
{% if obj.device_type.is_child_device and obj.parent_bay %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -24,14 +24,30 @@
|
|||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if perms.dcim.add_consoleporttemplate %}<li><a href="{% url 'dcim:consoleporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Console Ports</a></li>{% endif %}
|
{% if perms.dcim.add_consoleporttemplate %}
|
||||||
{% if perms.dcim.add_consoleserverporttemplate %}<li><a href="{% url 'dcim:consoleserverporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Console Server Ports</a></li>{% endif %}
|
<li><a href="{% url 'dcim:consoleporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
|
||||||
{% if perms.dcim.add_powerporttemplate %}<li><a href="{% url 'dcim:powerporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Power Ports</a></li>{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlettemplate %}<li><a href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Power Outlets</a></li>{% endif %}
|
{% if perms.dcim.add_consoleserverporttemplate %}
|
||||||
{% if perms.dcim.add_interfacetemplate %}<li><a href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Interfaces</a></li>{% endif %}
|
<li><a href="{% url 'dcim:consoleserverporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
|
||||||
{% if perms.dcim.add_frontporttemplate %}<li><a href="{% url 'dcim:frontporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Front Ports</a></li>{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_rearporttemplate %}<li><a href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Rear Ports</a></li>{% endif %}
|
{% if perms.dcim.add_powerporttemplate %}
|
||||||
{% if perms.dcim.add_devicebaytemplate %}<li><a href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Device Bays</a></li>{% endif %}
|
<li><a href="{% url 'dcim:powerporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_poweroutlettemplate %}
|
||||||
|
<li><a href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_interfacetemplate %}
|
||||||
|
<li><a href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_frontporttemplate %}
|
||||||
|
<li><a href="{% url 'dcim:frontporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_rearporttemplate %}
|
||||||
|
<li><a href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_devicebaytemplate %}
|
||||||
|
<li><a href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_devicebays">Device Bays</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -217,3 +233,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Redirect user to appropriate components tab if specified
|
||||||
|
var hash = document.location.hash;
|
||||||
|
var prefix = "tab_";
|
||||||
|
if (hash) {
|
||||||
|
$('.nav-tabs a[href="'+hash.replace(prefix,"")+'"]').tab('show');
|
||||||
|
}
|
||||||
|
$('.nav-tabs a').on('shown.bs.tab', function (e) {
|
||||||
|
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_consoleport %}
|
{% if perms.dcim.change_consoleport %}
|
||||||
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_consoleserverport %}
|
{% if perms.dcim.change_consoleserverport %}
|
||||||
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Install device"></i>
|
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Install device"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:devicebay_edit' pk=devicebay.pk %}" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:devicebay_edit' pk=devicebay.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit device bay"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit device bay"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_poweroutlet %}
|
{% if perms.dcim.change_poweroutlet %}
|
||||||
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" title="Edit outlet" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}?return_url={{ device.get_absolute_url }}" title="Edit outlet" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_powerport %}
|
{% if perms.dcim.change_powerport %}
|
||||||
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{% load helpers %}
|
|
||||||
<div class="rack_header">
|
|
||||||
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
|
|
||||||
{% if rack.role %}
|
|
||||||
<br /><small class="label" style="color: {{ rack.role.color|fgcolor }}; background-color: #{{ rack.role.color }}">{{ rack.role }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if rack.facility_id %}
|
|
||||||
<br /><small class="text-muted">{{ rack.facility_id }}</small>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
@@ -5,6 +5,16 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Interface</strong></div>
|
<div class="panel-heading"><strong>Interface</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% if form.instance.device %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required" for="id_device">Device</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">
|
||||||
|
<a href="{{ form.instance.device.get_absolute_url }}">{{ form.instance.device }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.label %}
|
{% render_field form.label %}
|
||||||
{% render_field form.type %}
|
{% render_field form.type %}
|
||||||
@@ -14,6 +24,11 @@
|
|||||||
{% render_field form.mtu %}
|
{% render_field form.mtu %}
|
||||||
{% render_field form.mgmt_only %}
|
{% render_field form.mgmt_only %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>802.1Q Switching</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
{% render_field form.mode %}
|
{% render_field form.mode %}
|
||||||
{% render_field form.untagged_vlan %}
|
{% render_field form.untagged_vlan %}
|
||||||
{% render_field form.tagged_vlans %}
|
{% render_field form.tagged_vlans %}
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
|
<li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
|
||||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
|
<li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
|
||||||
|
{% if rack.group %}
|
||||||
|
{% for group in rack.group.get_ancestors %}
|
||||||
|
<li><a href="{{ group.get_absolute_url }}">{{ group }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
<li><a href="{{ rack.group.get_absolute_url }}">{{ rack.group }}</a></li>
|
||||||
|
{% endif %}
|
||||||
<li>{{ rack }}</li>
|
<li>{{ rack }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +93,10 @@
|
|||||||
<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 }}</a>
|
{% for group in rack.group.get_ancestors %}
|
||||||
|
<a href="{{ group.get_absolute_url }}">{{ group }}</a> <i class="fa fa-caret-right"></i>
|
||||||
|
{% endfor %}
|
||||||
|
<a href="{{ rack.group.get_absolute_url }}">{{ rack.group }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -3,12 +3,18 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="btn-group pull-right noprint" role="group">
|
<div class="btn-toolbar pull-right noprint" role="toolbar">
|
||||||
<button class="btn btn-default toggle-images" selected="selected">
|
<button class="btn btn-default toggle-images" selected="selected">
|
||||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show Images
|
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show Images
|
||||||
</button>
|
</button>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-default{% if rack_face == 'front' %} active{% endif %}">Front</a>
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-default{% if rack_face == 'front' %} active{% endif %}">Front</a>
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-default{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-default{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse=None %}" class="btn btn-default{% if not reverse %} active{% endif %}">Normal</a>
|
||||||
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse='true' %}" class="btn btn-default{% if reverse %} active{% endif %}">Reversed</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Rack Elevations{% endblock %}</h1>
|
<h1>{% block title %}Rack Elevations{% endblock %}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -17,10 +23,23 @@
|
|||||||
<div style="white-space: nowrap; overflow-x: scroll;">
|
<div style="white-space: nowrap; overflow-x: scroll;">
|
||||||
{% for rack in page %}
|
{% for rack in page %}
|
||||||
<div style="display: inline-block; width: 266px">
|
<div style="display: inline-block; width: 266px">
|
||||||
{% include 'dcim/inc/rack_elevation_header.html' %}
|
<div class="text-center">
|
||||||
|
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
|
||||||
|
{% if rack.role %}
|
||||||
|
<br /><small class="label" style="color: {{ rack.role.color|fgcolor }}; background-color: #{{ rack.role.color }}">{{ rack.role }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if rack.facility_id %}
|
||||||
|
<br /><small class="text-muted">{{ rack.facility_id }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% include 'dcim/inc/rack_elevation.html' with face=rack_face %}
|
{% include 'dcim/inc/rack_elevation.html' with face=rack_face %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
{% include 'dcim/inc/rack_elevation_header.html' %}
|
<div class="text-center">
|
||||||
|
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
|
||||||
|
{% if rack.facility_id %}
|
||||||
|
<small class="text-muted">({{ rack.facility_id }})</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
|
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
|
||||||
{% if site.region %}
|
{% if site.region %}
|
||||||
{% for region in site.region.get_ancestors.unrestricted %}
|
{% for region in site.region.get_ancestors %}
|
||||||
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
|
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li><a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a></li>
|
<li><a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a></li>
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
<td>Region</td>
|
<td>Region</td>
|
||||||
<td>
|
<td>
|
||||||
{% if site.region %}
|
{% if site.region %}
|
||||||
{% for region in site.region.get_ancestors.unrestricted %}
|
{% for region in site.region.get_ancestors %}
|
||||||
<a href="{{ region.get_absolute_url }}">{{ region }}</a>
|
<a href="{{ region.get_absolute_url }}">{{ region }}</a>
|
||||||
<i class="fa fa-angle-right"></i>
|
<i class="fa fa-angle-right"></i>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -255,7 +255,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 }}</a></td>
|
<td style="padding-left: {{ rg.level }}8px"><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>
|
||||||
<td class="text-right noprint">
|
<td class="text-right noprint">
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}?group_id={{ rg.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
<a href="{% url 'dcim:rack_elevation_list' %}?group_id={{ rg.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
||||||
|
|||||||
@@ -5,14 +5,15 @@
|
|||||||
A module import error occurred during this request. Common causes include the following:
|
A module import error occurred during this request. Common causes include the following:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<i class="fa fa-warning"></i> <strong>Missing required packages</strong> - This installation of NetBox might be missing one or more required
|
<i class="fa fa-warning"></i> <strong>Missing required packages</strong> - This installation of NetBox might be
|
||||||
Python packages. These packages are listed in <code>requirements.txt</code> and are normally installed as part
|
missing one or more required Python packages. These packages are listed in <code>requirements.txt</code> and
|
||||||
of the installation or upgrade process. To verify installed packages, run <code>pip freeze</code> from the
|
<code>local_requirements.txt</code>, and are normally installed as part of the installation or upgrade process.
|
||||||
console and compare the output to the list of required packages.
|
To verify installed packages, run <code>pip freeze</code> from the console and compare the output to the list of
|
||||||
|
required packages.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<i class="fa fa-warning"></i> <strong>WSGI service not restarted after upgrade</strong> - If this installation has recently been upgraded,
|
<i class="fa fa-warning"></i> <strong>WSGI service not restarted after upgrade</strong> - If this installation
|
||||||
check that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This ensures that the new code is
|
has recently been upgraded, check that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This
|
||||||
running.
|
ensures that the new code is running.
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -276,7 +276,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Reports</strong>
|
<strong>Reports</strong>
|
||||||
</div>
|
</div>
|
||||||
{% if report_results and perms.extras.view_reportresult %}
|
{% if report_results and perms.extras.view_report %}
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for result in report_results %}
|
{% for result in report_results %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -285,7 +285,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
{% elif perms.extras.view_reportresult %}
|
{% elif perms.extras.view_report %}
|
||||||
<div class="panel-body text-muted">
|
<div class="panel-body text-muted">
|
||||||
None found
|
None found
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -518,7 +518,7 @@
|
|||||||
<li{% if not perms.extras.view_script %} class="disabled"{% endif %}>
|
<li{% if not perms.extras.view_script %} class="disabled"{% endif %}>
|
||||||
<a href="{% url 'extras:script_list' %}">Scripts</a>
|
<a href="{% url 'extras:script_list' %}">Scripts</a>
|
||||||
</li>
|
</li>
|
||||||
<li{% if not perms.extras.view_reportresult %} class="disabled"{% endif %}>
|
<li{% if not perms.extras.view_report %} class="disabled"{% endif %}>
|
||||||
<a href="{% url 'extras:report_list' %}">Reports</a>
|
<a href="{% url 'extras:report_list' %}">Reports</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
{% for section_name, menu_items in registry.plugin_menu_items.items %}
|
{% for section_name, menu_items in registry.plugin_menu_items.items %}
|
||||||
<li class="dropdown-header">{{ section_name }}</li>
|
<li class="dropdown-header">{{ section_name }}</li>
|
||||||
{% for menu_item in menu_items %}
|
{% for menu_item in menu_items %}
|
||||||
<li{% if menu_item.permissions and not request.user|has_perms:menu_item.permissions %} class="disabled"{% endif %}>
|
{% if not menu_item.permissions or request.user|has_perms:menu_item.permissions %}
|
||||||
|
<li>
|
||||||
{% if menu_item.buttons %}
|
{% if menu_item.buttons %}
|
||||||
<div class="buttons pull-right">
|
<div class="buttons pull-right">
|
||||||
{% for button in menu_item.buttons %}
|
{% for button in menu_item.buttons %}
|
||||||
@@ -17,6 +18,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url menu_item.link %}">{{ menu_item.link_text }}</a>
|
<a href="{% url menu_item.link %}">{{ menu_item.link_text }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a href="#">{{ menu_item.link_text }}</a></li>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if not forloop.last %}
|
{% if not forloop.last %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'ipam:ipaddress_add' %}{% querystring request %}">New IP</a>
|
<a href="{% url 'ipam:ipaddress_add' %}{% querystring request %}">New IP</a>
|
||||||
</li>
|
</li>
|
||||||
{% if 'interface' in request.GET %}
|
{% if 'interface' in request.GET or 'vminterface' in request.GET %}
|
||||||
<li role="presentation"{% if active_tab == 'assign' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'assign' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'ipam:ipaddress_assign' %}{% querystring request %}">Assign IP</a>
|
<a href="{% url 'ipam:ipaddress_assign' %}{% querystring request %}">Assign IP</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% load custom_links %}
|
{% load custom_links %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<div class="row noprint">
|
<div class="row noprint">
|
||||||
@@ -159,7 +160,24 @@
|
|||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
||||||
{% if duplicate_ips_table.rows %}
|
{% if duplicate_ips_table.rows %}
|
||||||
{% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}
|
{# Custom version of panel_table.html #}
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Duplicate IP Addresses</strong>
|
||||||
|
{% if more_duplicate_ips %}
|
||||||
|
<div class="pull-right">
|
||||||
|
<a type="button" class="btn btn-primary btn-xs"
|
||||||
|
{% if ipaddress.vrf %}
|
||||||
|
href="{% url 'ipam:ipaddress_list' %}?address={{ ipaddress.address.ip }}&vrf_id={{ ipaddress.vrf.pk }}"
|
||||||
|
{% else %}
|
||||||
|
href="{% url 'ipam:ipaddress_list' %}?address={{ ipaddress.address.ip }}&vrf_id=null"
|
||||||
|
{% endif %}
|
||||||
|
>Show all</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% render_table duplicate_ips_table 'inc/table.html' %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'utilities/obj_table.html' with table=related_ips_table table_template='panel_table.html' heading='Related IP Addresses' panel_class='default noprint' %}
|
{% include 'utilities/obj_table.html' with table=related_ips_table table_template='panel_table.html' heading='Related IP Addresses' panel_class='default noprint' %}
|
||||||
{% plugin_right_page ipaddress %}
|
{% plugin_right_page ipaddress %}
|
||||||
|
|||||||
@@ -52,8 +52,11 @@
|
|||||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||||
<a href="{% url 'ipam:vlan' pk=vlan.pk %}">VLAN</a>
|
<a href="{% url 'ipam:vlan' pk=vlan.pk %}">VLAN</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation"{% if active_tab == 'members' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'interfaces' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'ipam:vlan_members' pk=vlan.pk %}">Members <span class="badge">{{ vlan.get_members.count }}</span></a>
|
<a href="{% url 'ipam:vlan_interfaces' pk=vlan.pk %}">Device Interfaces <span class="badge">{{ vlan.get_interfaces.count }}</span></a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation"{% if active_tab == 'vminterfaces' %} class="active"{% endif %}>
|
||||||
|
<a href="{% url 'ipam:vlan_vminterfaces' pk=vlan.pk %}">VM Interfaces <span class="badge">{{ vlan.get_vminterfaces.count }}</span></a>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.extras.view_objectchange %}
|
{% if perms.extras.view_objectchange %}
|
||||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
{% extends 'ipam/vlan.html' %}
|
{% extends 'ipam/vlan.html' %}
|
||||||
|
|
||||||
{% block title %}{{ block.super }} - Members{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='VLAN Members' parent=vlan %}
|
{% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='Device Interfaces' parent=vlan %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
9
netbox/templates/ipam/vlan_vminterfaces.html
Normal file
9
netbox/templates/ipam/vlan_vminterfaces.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends 'ipam/vlan.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
{% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='Virtual Machine Interfaces' parent=vlan %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -31,7 +31,9 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>{{ obj_type|capfirst }}</strong></div>
|
<div class="panel-heading"><strong>{{ obj_type|capfirst }}</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% block form_fields %}
|
||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<tr class="interface{% if not iface.enabled %} danger{% endif %}" id="interface_{{ iface.name }}">
|
<tr class="interface{% if not iface.enabled %} danger{% endif %}" id="interface_{{ iface.name }}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
|
||||||
<td class="pk">
|
<td class="pk">
|
||||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
@@ -48,12 +48,12 @@
|
|||||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.virtualization.change_interface %}
|
{% if perms.virtualization.change_vminterface %}
|
||||||
<a href="{% url 'virtualization:vminterface_edit' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
<a href="{% url 'virtualization:vminterface_edit' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" 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 %}
|
||||||
{% if perms.virtualization.delete_interface %}
|
{% if perms.virtualization.delete_vminterface %}
|
||||||
<a href="{% url 'virtualization:vminterface_delete' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
<a href="{% url 'virtualization:vminterface_delete' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" 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>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
{% if ipaddresses %}
|
{% if ipaddresses %}
|
||||||
<tr class="ipaddresses">
|
<tr class="ipaddresses">
|
||||||
{# Placeholder #}
|
{# Placeholder #}
|
||||||
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
|
||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
{% block title %}Create {{ component_type }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="" method="post" class="form form-horizontal">
|
<form action="" method="post" class="form form-horizontal">
|
||||||
|
|||||||
@@ -5,14 +5,34 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Interface</strong></div>
|
<div class="panel-heading"><strong>Interface</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% if form.instance.virtual_machine %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required" for="id_device">Virtual Machine</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">
|
||||||
|
<a href="{{ form.instance.virtual_machine.get_absolute_url }}">{{ form.instance.virtual_machine }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.enabled %}
|
{% render_field form.enabled %}
|
||||||
{% render_field form.mac_address %}
|
{% render_field form.mac_address %}
|
||||||
{% render_field form.mtu %}
|
{% render_field form.mtu %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>802.1Q Switching</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
{% render_field form.mode %}
|
{% render_field form.mode %}
|
||||||
{% render_field form.untagged_vlan %}
|
{% render_field form.untagged_vlan %}
|
||||||
{% render_field form.tagged_vlans %}
|
{% render_field form.tagged_vlans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
{% render_field form.tags %}
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ObjectPermissionInline(admin.TabularInline):
|
|||||||
verbose_name_plural = 'Permissions'
|
verbose_name_plural = 'Permissions'
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super().get_queryset(request).prefetch_related('objectpermission__object_types')
|
return super().get_queryset(request).prefetch_related('objectpermission__object_types').nocache()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def object_types(instance):
|
def object_types(instance):
|
||||||
@@ -185,7 +185,7 @@ class ObjectPermissionForm(forms.ModelForm):
|
|||||||
|
|
||||||
# Validate the specified model constraints by attempting to execute a query. We don't care whether the query
|
# Validate the specified model constraints by attempting to execute a query. We don't care whether the query
|
||||||
# returns anything; we just want to make sure the specified constraints are valid.
|
# returns anything; we just want to make sure the specified constraints are valid.
|
||||||
if constraints:
|
if object_types and constraints:
|
||||||
# Normalize the constraints to a list of dicts
|
# Normalize the constraints to a list of dicts
|
||||||
if type(constraints) is not list:
|
if type(constraints) is not list:
|
||||||
constraints = [constraints]
|
constraints = [constraints]
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ class LoginView(View):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
form = LoginForm(request)
|
form = LoginForm(request)
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
logger = logging.getLogger('netbox.auth.login')
|
||||||
|
return self.redirect_to_next(request, logger)
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
@@ -49,12 +53,6 @@ class LoginView(View):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logger.debug("Login form validation was successful")
|
logger.debug("Login form validation was successful")
|
||||||
|
|
||||||
# Determine where to direct user after successful login
|
|
||||||
redirect_to = request.POST.get('next', reverse('home'))
|
|
||||||
if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
|
|
||||||
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")
|
|
||||||
redirect_to = reverse('home')
|
|
||||||
|
|
||||||
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
|
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
|
||||||
# last_login time upon authentication.
|
# last_login time upon authentication.
|
||||||
if settings.MAINTENANCE_MODE:
|
if settings.MAINTENANCE_MODE:
|
||||||
@@ -66,8 +64,7 @@ class LoginView(View):
|
|||||||
logger.info(f"User {request.user} successfully authenticated")
|
logger.info(f"User {request.user} successfully authenticated")
|
||||||
messages.info(request, "Logged in as {}.".format(request.user))
|
messages.info(request, "Logged in as {}.".format(request.user))
|
||||||
|
|
||||||
logger.debug(f"Redirecting user to {redirect_to}")
|
return self.redirect_to_next(request, logger)
|
||||||
return HttpResponseRedirect(redirect_to)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug("Login form validation failed")
|
logger.debug("Login form validation failed")
|
||||||
@@ -76,6 +73,19 @@ class LoginView(View):
|
|||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def redirect_to_next(self, request, logger):
|
||||||
|
if request.method == "POST":
|
||||||
|
redirect_to = request.POST.get('next', reverse('home'))
|
||||||
|
else:
|
||||||
|
redirect_to = request.GET.get('next', reverse('home'))
|
||||||
|
|
||||||
|
if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
|
||||||
|
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")
|
||||||
|
redirect_to = reverse('home')
|
||||||
|
|
||||||
|
logger.debug(f"Redirecting user to {redirect_to}")
|
||||||
|
return HttpResponseRedirect(redirect_to)
|
||||||
|
|
||||||
|
|
||||||
class LogoutView(View):
|
class LogoutView(View):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class APISelect(SelectWithDisabled):
|
|||||||
key = f'data-query-param-{name}'
|
key = f'data-query-param-{name}'
|
||||||
|
|
||||||
values = json.loads(self.attrs.get(key, '[]'))
|
values = json.loads(self.attrs.get(key, '[]'))
|
||||||
if type(value) is list:
|
if type(value) in (list, tuple):
|
||||||
values.extend([str(v) for v in value])
|
values.extend([str(v) for v in value])
|
||||||
else:
|
else:
|
||||||
values.append(str(value))
|
values.append(str(value))
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class Command(_Command):
|
|||||||
"This command is available for development purposes only. It will\n"
|
"This command is available for development purposes only. It will\n"
|
||||||
"NOT resolve any issues with missing or unapplied migrations. For assistance,\n"
|
"NOT resolve any issues with missing or unapplied migrations. For assistance,\n"
|
||||||
"please post to the NetBox mailing list:\n"
|
"please post to the NetBox mailing list:\n"
|
||||||
" https://groups.google.com/forum/#!forum/netbox-discuss"
|
" https://groups.google.com/g/netbox-discuss"
|
||||||
)
|
)
|
||||||
|
|
||||||
super().handle(*args, **kwargs)
|
super().handle(*args, **kwargs)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from django.contrib.postgres.aggregates import JSONBAgg
|
||||||
|
from django.contrib.postgres.aggregates.mixins import OrderableAggMixin
|
||||||
from django.db.models import F, Func
|
from django.db.models import F, Func
|
||||||
|
|
||||||
|
|
||||||
@@ -7,3 +9,21 @@ class CollateAsChar(Func):
|
|||||||
"""
|
"""
|
||||||
function = 'C'
|
function = 'C'
|
||||||
template = '(%(expressions)s) COLLATE "%(function)s"'
|
template = '(%(expressions)s) COLLATE "%(function)s"'
|
||||||
|
|
||||||
|
|
||||||
|
class OrderableJSONBAgg(OrderableAggMixin, JSONBAgg):
|
||||||
|
"""
|
||||||
|
TODO in Django 3.2 ordering is supported natively on JSONBAgg so this is no longer needed.
|
||||||
|
"""
|
||||||
|
template = '%(function)s(%(distinct)s%(expressions)s %(ordering)s)'
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyGroupByJSONBAgg(OrderableJSONBAgg):
|
||||||
|
"""
|
||||||
|
JSONBAgg is a builtin aggregation function which means it includes the use of a GROUP BY clause.
|
||||||
|
When used as an annotation for collecting config context data objects, the GROUP BY is
|
||||||
|
incorrect. This subclass overrides the Django ORM aggregation control to remove the GROUP BY.
|
||||||
|
|
||||||
|
TODO in Django 3.2 ordering is supported natively on JSONBAgg so we only need to inherit from JSONBAgg.
|
||||||
|
"""
|
||||||
|
contains_aggregate = False
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.db.models.fields.related import RelatedField
|
from django.db.models.fields.related import RelatedField
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@@ -44,7 +45,7 @@ class BaseTable(tables.Table):
|
|||||||
self.columns.show(name)
|
self.columns.show(name)
|
||||||
else:
|
else:
|
||||||
self.columns.hide(name)
|
self.columns.hide(name)
|
||||||
self.sequence = columns
|
self.sequence = [c for c in columns if c in self.base_columns]
|
||||||
|
|
||||||
# Always include PK and actions column, if defined on the table
|
# Always include PK and actions column, if defined on the table
|
||||||
if pk:
|
if pk:
|
||||||
@@ -63,7 +64,7 @@ class BaseTable(tables.Table):
|
|||||||
field_path = column.accessor.split('.')
|
field_path = column.accessor.split('.')
|
||||||
try:
|
try:
|
||||||
model_field = model._meta.get_field(field_path[0])
|
model_field = model._meta.get_field(field_path[0])
|
||||||
if isinstance(model_field, RelatedField):
|
if isinstance(model_field, (RelatedField, GenericForeignKey)):
|
||||||
prefetch_fields.append('__'.join(field_path))
|
prefetch_fields.append('__'.join(field_path))
|
||||||
except FieldDoesNotExist:
|
except FieldDoesNotExist:
|
||||||
pass
|
pass
|
||||||
@@ -114,12 +115,12 @@ class BooleanColumn(tables.Column):
|
|||||||
character.
|
character.
|
||||||
"""
|
"""
|
||||||
def render(self, value):
|
def render(self, value):
|
||||||
if value is True:
|
if value:
|
||||||
rendered = '<span class="text-success"><i class="fa fa-check"></i></span>'
|
rendered = '<span class="text-success"><i class="fa fa-check"></i></span>'
|
||||||
elif value is False:
|
elif value is None:
|
||||||
rendered = '<span class="text-danger"><i class="fa fa-close"></i></span>'
|
|
||||||
else:
|
|
||||||
rendered = '<span class="text-muted">—</span>'
|
rendered = '<span class="text-muted">—</span>'
|
||||||
|
else:
|
||||||
|
rendered = '<span class="text-danger"><i class="fa fa-close"></i></span>'
|
||||||
return mark_safe(rendered)
|
return mark_safe(rendered)
|
||||||
|
|
||||||
|
|
||||||
@@ -129,6 +130,7 @@ class ButtonsColumn(tables.TemplateColumn):
|
|||||||
|
|
||||||
:param model: Model class to use for calculating URL view names
|
:param model: Model class to use for calculating URL view names
|
||||||
:param prepend_content: Additional template content to render in the column (optional)
|
:param prepend_content: Additional template content to render in the column (optional)
|
||||||
|
:param return_url_extra: String to append to the return URL (e.g. for specifying a tab) (optional)
|
||||||
"""
|
"""
|
||||||
buttons = ('changelog', 'edit', 'delete')
|
buttons = ('changelog', 'edit', 'delete')
|
||||||
attrs = {'td': {'class': 'text-right text-nowrap noprint'}}
|
attrs = {'td': {'class': 'text-right text-nowrap noprint'}}
|
||||||
@@ -140,18 +142,19 @@ class ButtonsColumn(tables.TemplateColumn):
|
|||||||
</a>
|
</a>
|
||||||
{{% endif %}}
|
{{% endif %}}
|
||||||
{{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
|
{{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
|
||||||
<a href="{{% url '{app_label}:{model_name}_edit' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-warning" title="Edit">
|
<a href="{{% url '{app_label}:{model_name}_edit' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-xs btn-warning" title="Edit">
|
||||||
<i class="fa fa-pencil"></i>
|
<i class="fa fa-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
{{% endif %}}
|
{{% endif %}}
|
||||||
{{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
|
{{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
|
||||||
<a href="{{% url '{app_label}:{model_name}_delete' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger" title="Delete">
|
<a href="{{% url '{app_label}:{model_name}_delete' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-xs btn-danger" title="Delete">
|
||||||
<i class="fa fa-trash"></i>
|
<i class="fa fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
{{% endif %}}
|
{{% endif %}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, model, *args, pk_field='pk', buttons=None, prepend_template=None, **kwargs):
|
def __init__(self, model, *args, pk_field='pk', buttons=None, prepend_template=None, return_url_extra='',
|
||||||
|
**kwargs):
|
||||||
if prepend_template:
|
if prepend_template:
|
||||||
prepend_template = prepend_template.replace('{', '{{')
|
prepend_template = prepend_template.replace('{', '{{')
|
||||||
prepend_template = prepend_template.replace('}', '}}')
|
prepend_template = prepend_template.replace('}', '}}')
|
||||||
@@ -168,6 +171,7 @@ class ButtonsColumn(tables.TemplateColumn):
|
|||||||
|
|
||||||
self.extra_context.update({
|
self.extra_context.update({
|
||||||
'buttons': buttons or self.buttons,
|
'buttons': buttons or self.buttons,
|
||||||
|
'return_url_extra': return_url_extra,
|
||||||
})
|
})
|
||||||
|
|
||||||
def header(self):
|
def header(self):
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ class APIViewTestCases:
|
|||||||
response = self.client.patch(url, update_data, format='json', **self.header)
|
response = self.client.patch(url, update_data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
instance.refresh_from_db()
|
instance.refresh_from_db()
|
||||||
self.assertInstanceEqual(instance, self.update_data, api=True)
|
self.assertInstanceEqual(instance, update_data, api=True)
|
||||||
|
|
||||||
class DeleteObjectViewTestCase(APITestCase):
|
class DeleteObjectViewTestCase(APITestCase):
|
||||||
|
|
||||||
|
|||||||
@@ -945,7 +945,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
# ManyToManyFields
|
# ManyToManyFields
|
||||||
elif isinstance(model_field, ManyToManyField):
|
elif isinstance(model_field, ManyToManyField):
|
||||||
if form.cleaned_data[name].count() > 0:
|
if form.cleaned_data[name]:
|
||||||
getattr(obj, name).set(form.cleaned_data[name])
|
getattr(obj, name).set(form.cleaned_data[name])
|
||||||
# Normal fields
|
# Normal fields
|
||||||
elif form.cleaned_data[name] not in (None, ''):
|
elif form.cleaned_data[name] not in (None, ''):
|
||||||
@@ -1352,7 +1352,7 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
|||||||
for obj in data['pk']:
|
for obj in data['pk']:
|
||||||
|
|
||||||
names = data['name_pattern']
|
names = data['name_pattern']
|
||||||
labels = data['label_pattern']
|
labels = data['label_pattern'] if 'label_pattern' in data else None
|
||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
label = labels[i] if labels else None
|
label = labels[i] if labels else None
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user