mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-27 04:08:14 +01:00
Compare commits
134 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
869808b3f9 | ||
|
|
57ccbf44b8 | ||
|
|
416caa8f50 | ||
|
|
1e42fecf66 | ||
|
|
c9b00891ed | ||
|
|
497eacbea3 | ||
|
|
f90c591c78 | ||
|
|
175498940e | ||
|
|
eded00cbb3 | ||
|
|
e8d6281007 | ||
|
|
8299845615 | ||
|
|
9ae5865c2d | ||
|
|
c2d0cfdfc0 | ||
|
|
5dd252731e | ||
|
|
7b9436d2b9 | ||
|
|
6a369ac985 | ||
|
|
23d90823a3 | ||
|
|
4bfb6b476c | ||
|
|
0d60099588 | ||
|
|
94069e76c9 | ||
|
|
df9d67b873 | ||
|
|
f32e694499 | ||
|
|
e5900a3fe3 | ||
|
|
6e151b044d | ||
|
|
516bea6a0a | ||
|
|
496cabcc53 | ||
|
|
d051db5083 | ||
|
|
660fc23e15 | ||
|
|
a5a480133f | ||
|
|
68b544c676 | ||
|
|
a8c958ece2 | ||
|
|
f77f7ca0ec | ||
|
|
6b21c8453f | ||
|
|
fa8a8abc98 | ||
|
|
98cc36c458 | ||
|
|
f3beabba69 | ||
|
|
467fa5a847 | ||
|
|
50f283cf28 | ||
|
|
f49d7008a0 | ||
|
|
1fed564c47 | ||
|
|
bb99c3e6f9 | ||
|
|
8820cac792 | ||
|
|
ada911c20b | ||
|
|
17e01644f5 | ||
|
|
9458521f3e | ||
|
|
8aa73c5900 | ||
|
|
c0ca1eaf90 | ||
|
|
b29a5511df | ||
|
|
49e77841e0 | ||
|
|
daf6c8e327 | ||
|
|
9f8068e8d1 | ||
|
|
0b705553a5 | ||
|
|
a799094227 | ||
|
|
2f064cdfd1 | ||
|
|
6c28182dd3 | ||
|
|
3cb8c5db28 | ||
|
|
251abdb4dd | ||
|
|
726e4df54b | ||
|
|
bd32a6ac8e | ||
|
|
27d7400c36 | ||
|
|
53e52aeaa8 | ||
|
|
ae6ed97a80 | ||
|
|
3ad773beb3 | ||
|
|
be91235858 | ||
|
|
95fc0bbc94 | ||
|
|
9dad7e4daf | ||
|
|
d08ed9fe5f | ||
|
|
82210cc116 | ||
|
|
94d3e76517 | ||
|
|
3f72492a59 | ||
|
|
b7aa44837f | ||
|
|
7b7afd3e7b | ||
|
|
9c2514fce4 | ||
|
|
e04402ed57 | ||
|
|
3eda8d8482 | ||
|
|
79f2f03fb2 | ||
|
|
e5d7578663 | ||
|
|
773fd47ca6 | ||
|
|
830cf4b31f | ||
|
|
8f1acb700d | ||
|
|
7b1335825b | ||
|
|
11e2200acf | ||
|
|
f5356b84f6 | ||
|
|
1bf100ba15 | ||
|
|
7614f423e5 | ||
|
|
318c8b85e9 | ||
|
|
7085fe77da | ||
|
|
2e20d7f02b | ||
|
|
831065b5a1 | ||
|
|
b97167e841 | ||
|
|
19bacc9e23 | ||
|
|
61b61b1bc0 | ||
|
|
7c3318df92 | ||
|
|
d0b85586b9 | ||
|
|
cef0d168a5 | ||
|
|
3a192223a3 | ||
|
|
288a1d23e5 | ||
|
|
7c05db8e2f | ||
|
|
b7c0e8b71f | ||
|
|
a5ec0ee277 | ||
|
|
d528614cbf | ||
|
|
b5e8157700 | ||
|
|
24d6941cc4 | ||
|
|
0a62f75a40 | ||
|
|
a090955918 | ||
|
|
dfdeac4968 | ||
|
|
e84f2e3ad2 | ||
|
|
98ca4f5b5a | ||
|
|
87779b7b88 | ||
|
|
b56cae24c5 | ||
|
|
d48a68317d | ||
|
|
b07e88869a | ||
|
|
94bd27bcf5 | ||
|
|
090df05193 | ||
|
|
2c161c01c1 | ||
|
|
fc5a23cc88 | ||
|
|
73f2f9fc63 | ||
|
|
eb4b4a6c8d | ||
|
|
39430e01de | ||
|
|
96015aa590 | ||
|
|
c1720505f3 | ||
|
|
5c338a90a1 | ||
|
|
79cee12b1e | ||
|
|
aa5c42683a | ||
|
|
9c6938e7ae | ||
|
|
811c21ec7e | ||
|
|
84c14aadc7 | ||
|
|
f1f0d9cd0d | ||
|
|
e16942dea5 | ||
|
|
12efcec3b0 | ||
|
|
a7b6c40596 | ||
|
|
b95773938d | ||
|
|
6898ae7106 | ||
|
|
1a4f8c5422 |
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -13,11 +13,8 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: >
|
||||
What version of NetBox are you currently running? (If you don't have access to the most
|
||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||
before opening a bug report to see if your issue has already been addressed.)
|
||||
placeholder: v3.0.7
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.0.11
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.0.7
|
||||
placeholder: v3.0.11
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -76,14 +76,10 @@ free to add a comment with any additional justification for the feature.
|
||||
(However, note that comments with no substance other than a "+1" will be
|
||||
deleted. Please use GitHub's reactions feature to indicate your support.)
|
||||
|
||||
* Due to a large backlog of feature requests, we are not currently accepting
|
||||
any proposals which substantially extend NetBox's functionality beyond its
|
||||
current feature set. This includes the introduction of any new views or models
|
||||
which have not already been proposed in an existing feature request.
|
||||
|
||||
* Before filing a new feature request, consider raising your idea on the
|
||||
mailing list first. Feedback you receive there will help validate and shape the
|
||||
proposed feature before filing a formal issue.
|
||||
* Before filing a new feature request, consider raising your idea in a
|
||||
[GitHub discussion](https://github.com/netbox-community/netbox/discussions)
|
||||
first. Feedback you receive there will help validate and shape the proposed
|
||||
feature before filing a formal issue.
|
||||
|
||||
* Good feature requests are very narrowly defined. Be sure to thoroughly
|
||||
describe the functionality and data model(s) being proposed. The more effort
|
||||
|
||||
@@ -8,7 +8,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
|
||||
```shell
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -27,3 +27,13 @@ Device components represent discrete objects within a device which are used to t
|
||||
---
|
||||
|
||||
{!models/dcim/cable.md!}
|
||||
|
||||
In the example below, three individual cables comprise a path between devices A and D:
|
||||
|
||||

|
||||
|
||||
Traced from Interface 1 on Device A, NetBox will show the following path:
|
||||
|
||||
* Cable 1: Interface 1 to Front Port 1
|
||||
* Cable 2: Rear Port 1 to Rear Port 2
|
||||
* Cable 3: Front Port 2 to Interface 2
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
|
||||
# Example Power Topology
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{!models/extras/customlink.md!}
|
||||
@@ -240,7 +240,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
|
||||
!!! 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
|
||||
|
||||
@@ -259,6 +259,22 @@ http://netbox/api/extras/scripts/example.MyReport/ \
|
||||
--data '{"data": {"foo": "somevalue", "bar": 123}, "commit": true}'
|
||||
```
|
||||
|
||||
### Via the CLI
|
||||
|
||||
Scripts can be run on the CLI by invoking the management command:
|
||||
|
||||
```
|
||||
python3 manage.py runscript [--commit] [--loglevel {debug,info,warning,error,critical}] [--data "<data>"] <module>.<script>
|
||||
```
|
||||
|
||||
The required ``<module>.<script>`` argument is the script to run where ``<module>`` is the name of the python file in the ``scripts`` directory without the ``.py`` extension and ``<script>`` is the name of the script class in the ``<module>`` to run.
|
||||
|
||||
The optional ``--data "<data>"`` argument is the data to send to the script
|
||||
|
||||
The optional ``--loglevel`` argument is the desired logging level to output to the console.
|
||||
|
||||
The optional ``--commit`` argument will commit any changes in the script to the database.
|
||||
|
||||
## Example
|
||||
|
||||
Below is an example script that creates new objects for a planned site. The user is prompted for three variables:
|
||||
|
||||
@@ -21,9 +21,6 @@ This section entails the installation and configuration of a local PostgreSQL da
|
||||
sudo postgresql-setup --initdb
|
||||
```
|
||||
|
||||
!!! info
|
||||
PostgreSQL 9.6 and later are available natively on CentOS 8.2. If using an earlier CentOS release, you may need to [install it from an RPM](https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/).
|
||||
|
||||
CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below:
|
||||
|
||||
```no-highlight
|
||||
|
||||
@@ -17,8 +17,13 @@ Begin by installing all system packages required by NetBox and its dependencies.
|
||||
|
||||
=== "CentOS"
|
||||
|
||||
!!! warning
|
||||
CentOS 8 does not provide Python 3.7 or later via its native package manager. You will need to install it via some other means. [Here is an example](https://tecadmin.net/install-python-3-7-on-centos-8/) of installing Python 3.7 from source.
|
||||
|
||||
Once you have Python 3.7 or later installed, install the remaining system packages:
|
||||
|
||||
```no-highlight
|
||||
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
|
||||
sudo yum install -y gcc libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
|
||||
```
|
||||
|
||||
Before continuing with either platform, update pip (Python's package management tool) to its latest release:
|
||||
@@ -262,7 +267,7 @@ NetBox includes a `housekeeping` management command that handles some recurring
|
||||
A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to or linked from your system's daily cron task directory, or included within the crontab directly. (If installing NetBox into a nonstandard path, be sure to update the system paths within this script first.)
|
||||
|
||||
```shell
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Installation
|
||||
|
||||
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.2. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
|
||||
|
||||
The following sections detail how to set up a new instance of NetBox:
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ sudo systemctl restart netbox netbox-rq
|
||||
If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be linked from your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
|
||||
|
||||
```shell
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
||||
@@ -22,13 +22,3 @@ Each cable may be assigned a type, label, length, and color. Each cable is also
|
||||
## Tracing Cables
|
||||
|
||||
A cable may be traced from either of its endpoints by clicking the "trace" button. (A REST API endpoint also provides this functionality.) NetBox will follow the path of connected cables from this termination across the directly connected cable to the far-end termination. If the cable connects to a pass-through port, and the peer port has another cable connected, NetBox will continue following the cable path until it encounters a non-pass-through or unconnected termination point. The entire path will be displayed to the user.
|
||||
|
||||
In the example below, three individual cables comprise a path between devices A and D:
|
||||
|
||||

|
||||
|
||||
Traced from Interface 1 on Device A, NetBox will show the following path:
|
||||
|
||||
* Cable 1: Interface 1 to Front Port 1
|
||||
* Cable 2: Rear Port 1 to Rear Port 2
|
||||
* Cable 3: Front Port 2 to Interface 2
|
||||
|
||||
@@ -1,5 +1,102 @@
|
||||
# NetBox v3.0
|
||||
|
||||
## v3.0.11 (2021-11-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#2101](https://github.com/netbox-community/netbox/issues/2101) - Add missing `q` filters for necessary models
|
||||
* [#7424](https://github.com/netbox-community/netbox/issues/7424) - Add virtual chassis filters for device components
|
||||
* [#7531](https://github.com/netbox-community/netbox/issues/7531) - Add Markdown support for strikethrough formatting
|
||||
* [#7542](https://github.com/netbox-community/netbox/issues/7542) - Add optional VLAN group column to prefixes table
|
||||
* [#7803](https://github.com/netbox-community/netbox/issues/7803) - Improve live reloading of custom scripts
|
||||
* [#7810](https://github.com/netbox-community/netbox/issues/7810) - Add IEEE 802.15.1 interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7399](https://github.com/netbox-community/netbox/issues/7399) - Fix excessive CPU utilization when `AUTH_LDAP_FIND_GROUP_PERMS` is enabled
|
||||
* [#7657](https://github.com/netbox-community/netbox/issues/7657) - Make change logging middleware thread-safe
|
||||
* [#7720](https://github.com/netbox-community/netbox/issues/7720) - Fix initialization of custom script MultiObjectVar field with multiple values
|
||||
* [#7729](https://github.com/netbox-community/netbox/issues/7729) - Fix permissions evaluation when displaying VLAN group VLANs table
|
||||
* [#7739](https://github.com/netbox-community/netbox/issues/7739) - Fix exception when tracing cable across circuit with no far end termination
|
||||
* [#7813](https://github.com/netbox-community/netbox/issues/7813) - Fix handling of errors during export template rendering
|
||||
* [#7851](https://github.com/netbox-community/netbox/issues/7851) - Add missing cluster name filter for virtual machines
|
||||
* [#7857](https://github.com/netbox-community/netbox/issues/7857) - Fix ordering IP addresses by assignment status
|
||||
* [#7859](https://github.com/netbox-community/netbox/issues/7859) - Fix styling of form widgets under cable connection views
|
||||
* [#7864](https://github.com/netbox-community/netbox/issues/7864) - `power_port` can be null when creating power outlets via REST API
|
||||
* [#7865](https://github.com/netbox-community/netbox/issues/7865) - REST API should support null values for console port speeds
|
||||
|
||||
---
|
||||
|
||||
## v3.0.10 (2021-11-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7740](https://github.com/netbox-community/netbox/issues/7740) - Add mini-DIN 8 console port type
|
||||
* [#7760](https://github.com/netbox-community/netbox/issues/7760) - Add `vid` filter field to VLANs list
|
||||
* [#7767](https://github.com/netbox-community/netbox/issues/7767) - Add visual aids to interfaces table for type, enabled status
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7564](https://github.com/netbox-community/netbox/issues/7564) - Fix assignment of members to virtual chassis with initial position of zero
|
||||
* [#7701](https://github.com/netbox-community/netbox/issues/7701) - Fix conflation of assigned IP status & role in interface tables
|
||||
* [#7741](https://github.com/netbox-community/netbox/issues/7741) - Fix 404 when attaching multiple images in succession
|
||||
* [#7752](https://github.com/netbox-community/netbox/issues/7752) - Fix minimum version check under Python v3.10
|
||||
* [#7766](https://github.com/netbox-community/netbox/issues/7766) - Add missing outer dimension columns to rack table
|
||||
* [#7780](https://github.com/netbox-community/netbox/issues/7780) - Preserve multi-line values during CSV file import
|
||||
* [#7783](https://github.com/netbox-community/netbox/issues/7783) - Fix indentation of locations under site view
|
||||
* [#7788](https://github.com/netbox-community/netbox/issues/7788) - Improve XSS mitigation in Markdown renderer
|
||||
* [#7791](https://github.com/netbox-community/netbox/issues/7791) - Enable sorting device bays table by installed device status
|
||||
* [#7802](https://github.com/netbox-community/netbox/issues/7802) - Differentiate ID and VID columns in VLANs table
|
||||
* [#7808](https://github.com/netbox-community/netbox/issues/7808) - Fix reference values for content type under custom field import form
|
||||
* [#7809](https://github.com/netbox-community/netbox/issues/7809) - Add missing export template support for various models
|
||||
* [#7814](https://github.com/netbox-community/netbox/issues/7814) - Fix restriction of user & group objects in GraphQL API queries
|
||||
|
||||
---
|
||||
|
||||
## v3.0.9 (2021-11-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6529](https://github.com/netbox-community/netbox/issues/6529) - Introduce the `runscript` management command
|
||||
* [#6930](https://github.com/netbox-community/netbox/issues/6930) - Add an optional "ID" column to all tables
|
||||
* [#7668](https://github.com/netbox-community/netbox/issues/7668) - Add "view elevations" button to location view
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7599](https://github.com/netbox-community/netbox/issues/7599) - Improve color mode preference handling
|
||||
* [#7601](https://github.com/netbox-community/netbox/issues/7601) - Correct devices count for locations within global search results
|
||||
* [#7612](https://github.com/netbox-community/netbox/issues/7612) - Strip HTML from custom field descriptions
|
||||
* [#7628](https://github.com/netbox-community/netbox/issues/7628) - Fix `load_yaml` method for custom scripts
|
||||
* [#7643](https://github.com/netbox-community/netbox/issues/7643) - Fix circuit assignment when creating multiple terminations simultaneously
|
||||
* [#7644](https://github.com/netbox-community/netbox/issues/7644) - Prevent inadvertent deletion of prior change records when deleting objects (#7333 revisited)
|
||||
* [#7647](https://github.com/netbox-community/netbox/issues/7647) - Require interface assignment when designating IP address as primary for device/VM during CSV import
|
||||
* [#7664](https://github.com/netbox-community/netbox/issues/7664) - Preserve initial form data when bulk edit validation fails
|
||||
* [#7717](https://github.com/netbox-community/netbox/issues/7717) - Restore missing tags column on IP range table
|
||||
* [#7721](https://github.com/netbox-community/netbox/issues/7721) - Retain pagination preference when `MAX_PAGE_SIZE` is zero
|
||||
|
||||
---
|
||||
|
||||
## v3.0.8 (2021-10-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7551](https://github.com/netbox-community/netbox/issues/7551) - Add UI field to filter interfaces by kind
|
||||
* [#7561](https://github.com/netbox-community/netbox/issues/7561) - Add a utilization column to the IP ranges table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7300](https://github.com/netbox-community/netbox/issues/7300) - Fix incorrect Device LLDP interface row coloring
|
||||
* [#7495](https://github.com/netbox-community/netbox/issues/7495) - Fix navigation UI issue that caused improper element overlap
|
||||
* [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports
|
||||
* [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession
|
||||
* [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects
|
||||
* [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks
|
||||
* [#7550](https://github.com/netbox-community/netbox/issues/7550) - Fix rendering of UTF8-encoded data in change records
|
||||
* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of version when new release is available
|
||||
* [#7584](https://github.com/netbox-community/netbox/issues/7584) - Fix alignment of object identifier under object view
|
||||
|
||||
---
|
||||
|
||||
## v3.0.7 (2021-10-08)
|
||||
|
||||
### Enhancements
|
||||
@@ -352,7 +449,7 @@ Note that NetBox's `rqworker` process will _not_ service custom queues by defaul
|
||||
* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths
|
||||
* [#6328](https://github.com/netbox-community/netbox/issues/6328) - Build and serve documentation locally
|
||||
|
||||
### Bug Fixes (from v3.2-beta2)
|
||||
### Bug Fixes (from v3.0-beta2)
|
||||
|
||||
* [#6977](https://github.com/netbox-community/netbox/issues/6977) - Truncate global search dropdown on small screens
|
||||
* [#6979](https://github.com/netbox-community/netbox/issues/6979) - Hide "create & add another" button for circuit terminations
|
||||
|
||||
@@ -65,7 +65,7 @@ nav:
|
||||
- Customization:
|
||||
- Custom Fields: 'customization/custom-fields.md'
|
||||
- Custom Validation: 'customization/custom-validation.md'
|
||||
- Custom Links: 'customization/custom-links.md'
|
||||
- Custom Links: 'models/extras/customlink.md'
|
||||
- Export Templates: 'customization/export-templates.md'
|
||||
- Custom Scripts: 'customization/custom-scripts.md'
|
||||
- Reports: 'customization/reports.md'
|
||||
|
||||
@@ -11,6 +11,7 @@ def update_circuit(instance, **kwargs):
|
||||
When a CircuitTermination has been modified, update its parent Circuit.
|
||||
"""
|
||||
termination_name = f'termination_{instance.term_side.lower()}'
|
||||
instance.circuit.refresh_from_db()
|
||||
setattr(instance.circuit, termination_name, instance)
|
||||
instance.circuit.save()
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ class ProviderTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Provider
|
||||
fields = (
|
||||
'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'comments',
|
||||
'tags',
|
||||
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
|
||||
'comments', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
@@ -69,7 +69,7 @@ class ProviderNetworkTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ProviderNetwork
|
||||
fields = ('pk', 'name', 'provider', 'description', 'comments', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags')
|
||||
default_columns = ('pk', 'name', 'provider', 'description')
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class CircuitTypeTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ class CircuitTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
cid = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='ID'
|
||||
verbose_name='Circuit ID'
|
||||
)
|
||||
provider = tables.Column(
|
||||
linkify=True
|
||||
@@ -124,7 +124,7 @@ class CircuitTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
|
||||
'commit_rate', 'description', 'comments', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
|
||||
@@ -340,7 +340,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualChassis
|
||||
fields = ['id', 'name', 'url', 'master', 'member_count']
|
||||
fields = ['id', 'url', 'display', 'name', 'master', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -356,7 +356,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortTemplateSerializer(
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
@@ -538,7 +539,7 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerial
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -562,7 +563,7 @@ class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
||||
)
|
||||
speed = ChoiceField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -585,7 +586,8 @@ class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer,
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortSerializer(
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
|
||||
@@ -185,6 +185,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
TYPE_RJ11 = 'rj-11'
|
||||
TYPE_RJ12 = 'rj-12'
|
||||
TYPE_RJ45 = 'rj-45'
|
||||
TYPE_MINI_DIN_8 = 'mini-din-8'
|
||||
TYPE_USB_A = 'usb-a'
|
||||
TYPE_USB_B = 'usb-b'
|
||||
TYPE_USB_C = 'usb-c'
|
||||
@@ -202,6 +203,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
(TYPE_RJ11, 'RJ-11'),
|
||||
(TYPE_RJ12, 'RJ-12'),
|
||||
(TYPE_RJ45, 'RJ-45'),
|
||||
(TYPE_MINI_DIN_8, 'Mini-DIN 8'),
|
||||
)),
|
||||
('USB', (
|
||||
(TYPE_USB_A, 'USB Type A'),
|
||||
@@ -426,7 +428,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
)),
|
||||
('International/ITA', (
|
||||
(TYPE_ITA_C, 'ITA Type C (CEE 7/16)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/5)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/6)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE 7/4)'),
|
||||
(TYPE_ITA_EF, 'ITA Type E/F (CEE 7/7)'),
|
||||
(TYPE_ITA_G, 'ITA Type G (BS 1363)'),
|
||||
@@ -638,8 +640,8 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_CS8464C, 'CS8464C'),
|
||||
)),
|
||||
('ITA/International', (
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE7/5)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE7/3)'),
|
||||
(TYPE_ITA_E, 'ITA Type E (CEE 7/5)'),
|
||||
(TYPE_ITA_F, 'ITA Type F (CEE 7/3)'),
|
||||
(TYPE_ITA_G, 'ITA Type G (BS 1363)'),
|
||||
(TYPE_ITA_H, 'ITA Type H'),
|
||||
(TYPE_ITA_I, 'ITA Type I'),
|
||||
@@ -685,6 +687,18 @@ class PowerOutletFeedLegChoices(ChoiceSet):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceKindChoices(ChoiceSet):
|
||||
KIND_PHYSICAL = 'physical'
|
||||
KIND_VIRTUAL = 'virtual'
|
||||
KIND_WIRELESS = 'wireless'
|
||||
|
||||
CHOICES = (
|
||||
(KIND_PHYSICAL, 'Physical'),
|
||||
(KIND_VIRTUAL, 'Virtual'),
|
||||
(KIND_WIRELESS, 'Wireless'),
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTypeChoices(ChoiceSet):
|
||||
|
||||
# Virtual
|
||||
@@ -725,6 +739,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_80211AC = 'ieee802.11ac'
|
||||
TYPE_80211AD = 'ieee802.11ad'
|
||||
TYPE_80211AX = 'ieee802.11ax'
|
||||
TYPE_802151 = 'ieee802.15.1'
|
||||
|
||||
# Cellular
|
||||
TYPE_GSM = 'gsm'
|
||||
@@ -836,6 +851,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_80211AC, 'IEEE 802.11ac'),
|
||||
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||
(TYPE_80211AX, 'IEEE 802.11ax'),
|
||||
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
|
||||
@@ -861,6 +861,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__virtual_chassis',
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
label='Virtual Chassis (ID)'
|
||||
)
|
||||
virtual_chassis = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__virtual_chassis__name',
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Virtual Chassis',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -1394,6 +1405,10 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
|
||||
#
|
||||
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = MultiValueNumberFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__site_id'
|
||||
@@ -1416,6 +1431,15 @@ class ConnectionFilterSet(BaseFilterSet):
|
||||
return queryset
|
||||
return queryset.filter(**{f'{name}__in': value})
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(device__name__icontains=value) |
|
||||
Q(cable__label__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class ConsoleConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
|
||||
@@ -215,8 +215,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm)
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
|
||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||
@@ -277,8 +276,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
class Meta(ConnectCableToDeviceForm.Meta):
|
||||
fields = [
|
||||
'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
|
||||
'color', 'length', 'length_unit', 'tags',
|
||||
|
||||
@@ -92,12 +92,19 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||
label=_('Location'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
virtual_chassis_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
required=False,
|
||||
label=_('Virtual Chassis'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
'virtual_chassis_id': '$virtual_chassis_id'
|
||||
},
|
||||
label=_('Device'),
|
||||
fetch_trigger='open'
|
||||
@@ -888,7 +895,7 @@ class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -908,7 +915,7 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'speed'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -928,7 +935,7 @@ class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
@@ -943,7 +950,7 @@ class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
@@ -957,9 +964,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
kind = forms.MultipleChoiceField(
|
||||
choices=InterfaceKindChoices,
|
||||
required=False,
|
||||
widget=StaticSelectMultiple()
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
required=False,
|
||||
@@ -988,7 +1000,7 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
model = FrontPort
|
||||
type = forms.MultipleChoiceField(
|
||||
@@ -1007,7 +1019,7 @@ class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'type', 'color'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
@@ -1025,7 +1037,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1035,7 +1047,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
|
||||
]
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -1063,6 +1075,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
#
|
||||
|
||||
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -1090,6 +1107,11 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
|
||||
|
||||
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -1117,6 +1139,11 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
|
||||
|
||||
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
|
||||
label=_('Search')
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
|
||||
@@ -117,12 +117,18 @@ class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
|
||||
raise forms.ValidationError({
|
||||
'initial_position': "A position must be specified for the first VC member."
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
# Assign VC members
|
||||
if instance.pk:
|
||||
initial_position = self.cleaned_data.get('initial_position') or 1
|
||||
if instance.pk and self.cleaned_data['members']:
|
||||
initial_position = self.cleaned_data.get('initial_position', 1)
|
||||
for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
|
||||
member.virtual_chassis = instance
|
||||
member.vc_position = i
|
||||
|
||||
@@ -442,15 +442,16 @@ class CableTraceSVG:
|
||||
parent_objects.append(parent_object)
|
||||
|
||||
# Near end termination
|
||||
termination = self._draw_box(
|
||||
width=self.width * .8,
|
||||
color=self._get_color(near_end),
|
||||
url=near_end.get_absolute_url(),
|
||||
labels=self._get_labels(near_end),
|
||||
y_indent=PADDING,
|
||||
radius=5
|
||||
)
|
||||
terminations.append(termination)
|
||||
if near_end is not None:
|
||||
termination = self._draw_box(
|
||||
width=self.width * .8,
|
||||
color=self._get_color(near_end),
|
||||
url=near_end.get_absolute_url(),
|
||||
labels=self._get_labels(near_end),
|
||||
y_indent=PADDING,
|
||||
radius=5
|
||||
)
|
||||
terminations.append(termination)
|
||||
|
||||
# Connector (either a Cable or attachment to a ProviderNetwork)
|
||||
if connector is not None:
|
||||
|
||||
@@ -43,6 +43,7 @@ class ConsoleConnectionTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class PowerConnectionTable(BaseTable):
|
||||
@@ -73,6 +74,7 @@ class PowerConnectionTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class InterfaceConnectionTable(BaseTable):
|
||||
@@ -106,3 +108,4 @@ class InterfaceConnectionTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')
|
||||
exclude = ('id', )
|
||||
|
||||
@@ -16,10 +16,6 @@ __all__ = (
|
||||
|
||||
class CableTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
id = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='ID'
|
||||
)
|
||||
termination_a_parent = tables.TemplateColumn(
|
||||
template_code=CABLE_TERMINATION_PARENT,
|
||||
accessor=Accessor('termination_a'),
|
||||
|
||||
@@ -53,6 +53,14 @@ def get_cabletermination_row_class(record):
|
||||
return ''
|
||||
|
||||
|
||||
def get_interface_row_class(record):
|
||||
if not record.enabled:
|
||||
return 'danger'
|
||||
elif record.is_virtual:
|
||||
return 'primary'
|
||||
return get_cabletermination_row_class(record)
|
||||
|
||||
|
||||
def get_interface_state_attribute(record):
|
||||
"""
|
||||
Get interface enabled state as string to attach to <tr/> DOM element.
|
||||
@@ -88,7 +96,7 @@ class DeviceRoleTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -116,7 +124,7 @@ class PlatformTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
fields = (
|
||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
||||
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
||||
'description', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -196,7 +204,7 @@ class DeviceTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = (
|
||||
'pk', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
||||
'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags',
|
||||
)
|
||||
@@ -227,7 +235,7 @@ class DeviceImportTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
fields = ('id', 'name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
empty_text = False
|
||||
|
||||
|
||||
@@ -290,7 +298,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||
@@ -311,7 +319,7 @@ class DeviceConsolePortTable(ConsolePortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags', 'actions'
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||
@@ -334,7 +342,7 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
|
||||
@@ -356,7 +364,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
|
||||
@@ -379,7 +387,7 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
|
||||
'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
@@ -401,7 +409,7 @@ class DevicePowerPortTable(PowerPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -430,7 +438,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||
@@ -451,7 +459,7 @@ class DevicePowerOutletTable(PowerOutletTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -492,7 +500,7 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'pk', 'id', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans',
|
||||
)
|
||||
@@ -501,8 +509,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
|
||||
|
||||
class DeviceInterfaceTable(InterfaceTable):
|
||||
name = tables.TemplateColumn(
|
||||
template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant'
|
||||
'{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet'
|
||||
template_code='<i class="mdi mdi-{% if record.mgmt_only %}wrench{% elif record.is_lag %}reorder-horizontal'
|
||||
'{% elif record.is_virtual %}circle{% elif record.is_wireless %}wifi{% else %}ethernet'
|
||||
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||
order_by=Accessor('_name'),
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
@@ -524,7 +532,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'pk', 'id', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
@@ -534,7 +542,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
'cable', 'connection', 'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': get_cabletermination_row_class,
|
||||
'class': get_interface_row_class,
|
||||
'data-name': lambda record: record.name,
|
||||
'data-enabled': get_interface_state_attribute,
|
||||
}
|
||||
@@ -561,7 +569,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'cable_peer', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -585,7 +593,7 @@ class DeviceFrontPortTable(FrontPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -612,7 +620,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
|
||||
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'cable_peer', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
|
||||
@@ -634,7 +642,7 @@ class DeviceRearPortTable(RearPortTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'pk', 'id', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'cable_peer', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -653,7 +661,8 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
}
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code=DEVICEBAY_STATUS
|
||||
template_code=DEVICEBAY_STATUS,
|
||||
order_by=Accessor('installed_device__status')
|
||||
)
|
||||
installed_device = tables.Column(
|
||||
linkify=True
|
||||
@@ -664,7 +673,7 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags')
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
|
||||
|
||||
|
||||
@@ -684,7 +693,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
|
||||
'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'status', 'installed_device', 'description', 'actions',
|
||||
@@ -710,7 +719,7 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'pk', 'id', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'discovered', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
@@ -731,7 +740,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
|
||||
'pk', 'id', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered',
|
||||
'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -763,5 +772,5 @@ class VirtualChassisTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'name', 'domain', 'master', 'member_count', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
||||
|
||||
@@ -46,6 +46,9 @@ class ManufacturerTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
|
||||
)
|
||||
|
||||
@@ -76,7 +79,7 @@ class DeviceTypeTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = (
|
||||
'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
'comments', 'instance_count', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -90,10 +93,16 @@ class DeviceTypeTable(BaseTable):
|
||||
|
||||
class ComponentTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
id = tables.Column(
|
||||
verbose_name='ID'
|
||||
)
|
||||
name = tables.Column(
|
||||
order_by=('_name',)
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
actions = ButtonsColumn(
|
||||
@@ -102,7 +111,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_consoleports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = ConsolePortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -115,7 +124,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_consoleserverports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -128,7 +137,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_powerports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = PowerPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -141,7 +150,7 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_poweroutlets'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = PowerOutletTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -157,7 +166,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_interfaces'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = InterfaceTemplate
|
||||
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -174,7 +183,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_frontports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = FrontPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -188,7 +197,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_rearports'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = RearPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
@@ -201,7 +210,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
return_url_extra='%23tab_devicebays'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = DeviceBayTemplate
|
||||
fields = ('pk', 'name', 'label', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
@@ -33,7 +33,7 @@ class PowerPanelTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPanel
|
||||
fields = ('pk', 'name', 'site', 'location', 'powerfeed_count', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class PowerFeedTable(CableTerminationTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power',
|
||||
'comments', 'tags',
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ class RackRoleTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackRole
|
||||
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -72,12 +72,20 @@ class RackTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:rack_list'
|
||||
)
|
||||
outer_width = tables.TemplateColumn(
|
||||
template_code="{{ record.outer_width }} {{ record.outer_unit }}",
|
||||
verbose_name='Outer Width'
|
||||
)
|
||||
outer_depth = tables.TemplateColumn(
|
||||
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
|
||||
verbose_name='Outer Depth'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'width', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
|
||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
|
||||
'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
@@ -115,7 +123,7 @@ class RackReservationTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
fields = (
|
||||
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
|
||||
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
|
||||
'actions',
|
||||
)
|
||||
default_columns = (
|
||||
|
||||
@@ -33,7 +33,7 @@ class RegionTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Region
|
||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class SiteGroupTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SiteGroup
|
||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class SiteTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
fields = (
|
||||
'pk', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description',
|
||||
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description',
|
||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||
'contact_email', 'comments', 'tags',
|
||||
)
|
||||
@@ -120,5 +120,5 @@ class LocationTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Location
|
||||
fields = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'site', 'rack_count', 'device_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'actions')
|
||||
|
||||
@@ -40,17 +40,13 @@ DEVICEBAY_STATUS = """
|
||||
|
||||
INTERFACE_IPADDRESSES = """
|
||||
<div class="table-badge-group">
|
||||
{% for ip in record.ip_addresses.all %}
|
||||
<a
|
||||
class="table-badge{% if ip.status != 'active' %} badge bg-{{ ip.get_status_class }}{% elif ip.role %} badge bg-{{ ip.get_role_class }}{% endif %}"
|
||||
href="{{ ip.get_absolute_url }}"
|
||||
{% if ip.status != 'active'%}data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}"
|
||||
{% elif ip.role %}data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_role_display }}"
|
||||
{% endif %}
|
||||
>
|
||||
{{ ip }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% for ip in record.ip_addresses.all %}
|
||||
{% if ip.status != 'active' %}
|
||||
<a href="{{ ip.get_absolute_url }}" class="table-badge badge bg-{{ ip.get_status_class }}" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}">{{ ip }}</a>
|
||||
{% else %}
|
||||
<a href="{{ ip.get_absolute_url }}" class="table-badge">{{ ip }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
@@ -584,6 +584,12 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||
)
|
||||
|
||||
power_port_templates = (
|
||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
|
||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'),
|
||||
)
|
||||
PowerPortTemplate.objects.bulk_create(power_port_templates)
|
||||
|
||||
power_outlet_templates = (
|
||||
PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 1'),
|
||||
PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 2'),
|
||||
@@ -595,14 +601,17 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Power Outlet Template 4',
|
||||
'power_port': power_port_templates[0].pk,
|
||||
},
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Power Outlet Template 5',
|
||||
'power_port': power_port_templates[1].pk,
|
||||
},
|
||||
{
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Power Outlet Template 6',
|
||||
'power_port': None,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1033,14 +1042,17 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Port 4',
|
||||
'speed': 9600,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Port 5',
|
||||
'speed': 115200,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Port 6',
|
||||
'speed': None,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1072,14 +1084,17 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Server Port 4',
|
||||
'speed': 9600,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Server Port 5',
|
||||
'speed': 115200,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Console Server Port 6',
|
||||
'speed': None,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1139,6 +1154,12 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
devicerole = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
|
||||
device = Device.objects.create(device_type=devicetype, device_role=devicerole, name='Device 1', site=site)
|
||||
|
||||
power_ports = (
|
||||
PowerPort(device=device, name='Power Port 1'),
|
||||
PowerPort(device=device, name='Power Port 2'),
|
||||
)
|
||||
PowerPort.objects.bulk_create(power_ports)
|
||||
|
||||
power_outlets = (
|
||||
PowerOutlet(device=device, name='Power Outlet 1'),
|
||||
PowerOutlet(device=device, name='Power Outlet 2'),
|
||||
@@ -1150,14 +1171,17 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Power Outlet 4',
|
||||
'power_port': power_ports[0].pk,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Power Outlet 5',
|
||||
'power_port': power_ports[1].pk,
|
||||
},
|
||||
{
|
||||
'device': device.pk,
|
||||
'name': 'Power Outlet 6',
|
||||
'power_port': None,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1524,7 +1548,7 @@ class ConnectedDeviceTest(APITestCase):
|
||||
|
||||
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualChassis
|
||||
brief_fields = ['id', 'master', 'member_count', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'master', 'member_count', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -2048,6 +2048,11 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
# VirtualChassis assignment for filtering
|
||||
virtual_chassis = VirtualChassis.objects.create(master=devices[0])
|
||||
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
|
||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'),
|
||||
Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'),
|
||||
@@ -2157,6 +2162,10 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_chassis_id(self):
|
||||
params = {'virtual_chassis_id': [VirtualChassis.objects.first().pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_device(self):
|
||||
devices = Device.objects.all()[:2]
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
|
||||
@@ -2,8 +2,9 @@ from contextlib import contextmanager
|
||||
|
||||
from django.db.models.signals import m2m_changed, pre_delete, post_save
|
||||
|
||||
from extras.signals import clear_webhooks, _clear_webhook_queue, _handle_changed_object, _handle_deleted_object
|
||||
from utilities.utils import curry
|
||||
from extras.signals import clear_webhooks, clear_webhook_queue, handle_changed_object, handle_deleted_object
|
||||
from netbox import thread_locals
|
||||
from netbox.request_context import set_request
|
||||
from .webhooks import flush_webhooks
|
||||
|
||||
|
||||
@@ -15,12 +16,8 @@ def change_logging(request):
|
||||
|
||||
:param request: WSGIRequest object with a unique `id` set
|
||||
"""
|
||||
webhook_queue = []
|
||||
|
||||
# Curry signals receivers to pass the current request
|
||||
handle_changed_object = curry(_handle_changed_object, request, webhook_queue)
|
||||
handle_deleted_object = curry(_handle_deleted_object, request, webhook_queue)
|
||||
clear_webhook_queue = curry(_clear_webhook_queue, webhook_queue)
|
||||
set_request(request)
|
||||
thread_locals.webhook_queue = []
|
||||
|
||||
# Connect our receivers to the post_save and post_delete signals.
|
||||
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
|
||||
@@ -38,5 +35,8 @@ def change_logging(request):
|
||||
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
|
||||
|
||||
# Flush queued webhooks to RQ
|
||||
flush_webhooks(webhook_queue)
|
||||
del webhook_queue
|
||||
flush_webhooks(thread_locals.webhook_queue)
|
||||
del thread_locals.webhook_queue
|
||||
|
||||
# Clear the request from thread-local storage
|
||||
set_request(None)
|
||||
|
||||
@@ -15,6 +15,7 @@ from .models import *
|
||||
__all__ = (
|
||||
'ConfigContextFilterSet',
|
||||
'ContentTypeFilterSet',
|
||||
'CustomFieldFilterSet',
|
||||
'CustomLinkFilterSet',
|
||||
'ExportTemplateFilterSet',
|
||||
'ImageAttachmentFilterSet',
|
||||
@@ -34,6 +35,10 @@ EXACT_FILTER_TYPES = (
|
||||
|
||||
|
||||
class WebhookFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
content_types = ContentTypeFilter()
|
||||
http_method = django_filters.MultipleChoiceFilter(
|
||||
choices=WebhookHttpMethodChoices
|
||||
@@ -46,30 +51,81 @@ class WebhookFilterSet(BaseFilterSet):
|
||||
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(payload_url__icontains=value)
|
||||
)
|
||||
|
||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
|
||||
class CustomFieldFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
content_types = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(label__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class CustomLinkFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(link_text__icontains=value) |
|
||||
Q(link_url__icontains=value) |
|
||||
Q(group_name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ExportTemplateFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['id', 'content_type', 'name']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
created = django_filters.DateTimeFilter()
|
||||
content_type = ContentTypeFilter()
|
||||
|
||||
@@ -77,6 +133,11 @@ class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
model = ImageAttachment
|
||||
fields = ['id', 'content_type_id', 'object_id', 'name']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(name__icontains=value)
|
||||
|
||||
|
||||
class JournalEntryFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
|
||||
@@ -70,7 +70,7 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||
class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
limit_choices_to=FeatureQuery('export_templates')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
158
netbox/extras/management/commands/runscript.py
Normal file
158
netbox/extras/management/commands/runscript.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
from extras.api.serializers import ScriptOutputSerializer
|
||||
from extras.choices import JobResultStatusChoices
|
||||
from extras.context_managers import change_logging
|
||||
from extras.models import JobResult
|
||||
from extras.scripts import get_script
|
||||
from utilities.exceptions import AbortTransaction
|
||||
from utilities.utils import NetBoxFakeRequest
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Run a script in Netbox"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--loglevel',
|
||||
help="Logging Level (default: info)",
|
||||
dest='loglevel',
|
||||
default='info',
|
||||
choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--commit', help="Commit this script to database", action='store_true')
|
||||
parser.add_argument('--user', help="User script is running as")
|
||||
parser.add_argument('--data', help="Data as a string encapsulated JSON blob")
|
||||
parser.add_argument('script', help="Script to run")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
def _run_script():
|
||||
"""
|
||||
Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with
|
||||
the change_logging context manager (which is bypassed if commit == False).
|
||||
"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
script.output = script.run(data=data, commit=commit)
|
||||
job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED)
|
||||
|
||||
if not commit:
|
||||
raise AbortTransaction()
|
||||
|
||||
except AbortTransaction:
|
||||
script.log_info("Database changes have been reverted automatically.")
|
||||
|
||||
except Exception as e:
|
||||
stacktrace = traceback.format_exc()
|
||||
script.log_failure(
|
||||
f"An exception occurred: `{type(e).__name__}: {e}`\n```\n{stacktrace}\n```"
|
||||
)
|
||||
script.log_info("Database changes have been reverted due to error.")
|
||||
logger.error(f"Exception raised during script execution: {e}")
|
||||
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
|
||||
|
||||
finally:
|
||||
job_result.data = ScriptOutputSerializer(script).data
|
||||
job_result.save()
|
||||
|
||||
logger.info(f"Script completed in {job_result.duration}")
|
||||
|
||||
# Params
|
||||
script = options['script']
|
||||
loglevel = options['loglevel']
|
||||
commit = options['commit']
|
||||
try:
|
||||
data = json.loads(options['data'])
|
||||
except TypeError:
|
||||
data = {}
|
||||
|
||||
module, name = script.split('.', 1)
|
||||
|
||||
# Take user from command line if provided and exists, other
|
||||
if options['user']:
|
||||
try:
|
||||
user = User.objects.get(username=options['user'])
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.filter(is_superuser=True).order_by('pk')[0]
|
||||
else:
|
||||
user = User.objects.filter(is_superuser=True).order_by('pk')[0]
|
||||
|
||||
# Setup logging to Stdout
|
||||
formatter = logging.Formatter(f'[%(asctime)s][%(levelname)s] - %(message)s')
|
||||
stdouthandler = logging.StreamHandler(sys.stdout)
|
||||
stdouthandler.setLevel(logging.DEBUG)
|
||||
stdouthandler.setFormatter(formatter)
|
||||
|
||||
logger = logging.getLogger(f"netbox.scripts.{module}.{name}")
|
||||
logger.addHandler(stdouthandler)
|
||||
|
||||
try:
|
||||
logger.setLevel({
|
||||
'critical': logging.CRITICAL,
|
||||
'debug': logging.DEBUG,
|
||||
'error': logging.ERROR,
|
||||
'fatal': logging.FATAL,
|
||||
'info': logging.INFO,
|
||||
'warning': logging.WARNING,
|
||||
}[loglevel])
|
||||
except KeyError:
|
||||
raise CommandError(f"Invalid log level: {loglevel}")
|
||||
|
||||
# Get the script
|
||||
script = get_script(module, name)()
|
||||
# Parse the parameters
|
||||
form = script.as_form(data, None)
|
||||
|
||||
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
||||
|
||||
# Delete any previous terminal state results
|
||||
JobResult.objects.filter(
|
||||
obj_type=script_content_type,
|
||||
name=script.full_name,
|
||||
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
||||
).delete()
|
||||
|
||||
# Create the job result
|
||||
job_result = JobResult.objects.create(
|
||||
name=script.full_name,
|
||||
obj_type=script_content_type,
|
||||
user=User.objects.filter(is_superuser=True).order_by('pk')[0],
|
||||
job_id=uuid.uuid4()
|
||||
)
|
||||
|
||||
request = NetBoxFakeRequest({
|
||||
'META': {},
|
||||
'POST': data,
|
||||
'GET': {},
|
||||
'FILES': {},
|
||||
'user': user,
|
||||
'path': '',
|
||||
'id': job_result.job_id
|
||||
})
|
||||
|
||||
if form.is_valid():
|
||||
job_result.status = JobResultStatusChoices.STATUS_RUNNING
|
||||
job_result.save()
|
||||
|
||||
logger.info(f"Running script (commit={commit})")
|
||||
script.request = request
|
||||
|
||||
# Execute the script. If commit is True, wrap it with the change_logging context manager to ensure we process
|
||||
# change logging, webhooks, etc.
|
||||
with change_logging(request):
|
||||
_run_script()
|
||||
else:
|
||||
logger.error('Data is not valid:')
|
||||
for field, errors in form.errors.get_json_data().items():
|
||||
for error in errors:
|
||||
logger.error(f'\t{field}: {error.get("message")}')
|
||||
job_result.status = JobResultStatusChoices.STATUS_ERRORED
|
||||
job_result.save()
|
||||
@@ -7,6 +7,7 @@ from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.validators import RegexValidator, ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from extras.choices import *
|
||||
@@ -30,7 +31,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
||||
return self.get_queryset().filter(content_types=content_type)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class CustomField(ChangeLoggedModel):
|
||||
content_types = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
@@ -287,7 +288,7 @@ class CustomField(ChangeLoggedModel):
|
||||
field.model = self
|
||||
field.label = str(self)
|
||||
if self.description:
|
||||
field.help_text = self.description
|
||||
field.help_text = escape(self.description)
|
||||
|
||||
return field
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.db import models
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format, time_format
|
||||
from django.utils.formats import date_format
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from extras.choices import *
|
||||
@@ -36,7 +36,7 @@ __all__ = (
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class Webhook(ChangeLoggedModel):
|
||||
"""
|
||||
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
|
||||
@@ -175,7 +175,7 @@ class Webhook(ChangeLoggedModel):
|
||||
# Custom links
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class CustomLink(ChangeLoggedModel):
|
||||
"""
|
||||
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
|
||||
@@ -234,7 +234,7 @@ class CustomLink(ChangeLoggedModel):
|
||||
# Export templates
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class ExportTemplate(ChangeLoggedModel):
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
@@ -357,6 +357,8 @@ class ImageAttachment(BigIDModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = ('content_type', 'object_id')
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # name may be non-unique
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from utilities.querysets import RestrictedQuerySet
|
||||
# Tags
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
@extras_features('webhooks', 'export_templates')
|
||||
class Tag(ChangeLoggedModel, TagBase):
|
||||
color = ColorField(
|
||||
default=ColorChoices.COLOR_GREY
|
||||
|
||||
@@ -3,8 +3,8 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
@@ -345,9 +345,14 @@ class BaseScript:
|
||||
"""
|
||||
Return data from a YAML file
|
||||
"""
|
||||
try:
|
||||
from yaml import CLoader as Loader
|
||||
except ImportError:
|
||||
from yaml import Loader
|
||||
|
||||
file_path = os.path.join(settings.SCRIPTS_ROOT, filename)
|
||||
with open(file_path, 'r') as datafile:
|
||||
data = yaml.load(datafile)
|
||||
data = yaml.load(datafile, Loader=Loader)
|
||||
|
||||
return data
|
||||
|
||||
@@ -473,6 +478,10 @@ def get_scripts(use_names=False):
|
||||
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
||||
# defined.
|
||||
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
||||
# Remove cached module to ensure consistency with filesystem
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
|
||||
module = importer.find_module(module_name).load_module(module_name)
|
||||
if use_names and hasattr(module, 'name'):
|
||||
module_name = module.name
|
||||
|
||||
@@ -6,6 +6,8 @@ from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||
from django.dispatch import receiver, Signal
|
||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||
|
||||
from netbox import thread_locals
|
||||
from netbox.request_context import get_request
|
||||
from netbox.signals import post_clean
|
||||
from .choices import ObjectChangeActionChoices
|
||||
from .models import CustomField, ObjectChange
|
||||
@@ -20,10 +22,16 @@ from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
|
||||
clear_webhooks = Signal()
|
||||
|
||||
|
||||
def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
def handle_changed_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Fires when an object is created or updated.
|
||||
"""
|
||||
if not hasattr(instance, 'to_objectchange'):
|
||||
return
|
||||
|
||||
request = get_request()
|
||||
m2m_changed = False
|
||||
|
||||
def is_same_object(instance, webhook_data):
|
||||
return (
|
||||
ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and
|
||||
@@ -31,11 +39,6 @@ def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
request.id == webhook_data['request_id']
|
||||
)
|
||||
|
||||
if not hasattr(instance, 'to_objectchange'):
|
||||
return
|
||||
|
||||
m2m_changed = False
|
||||
|
||||
# Determine the type of change being made
|
||||
if kwargs.get('created'):
|
||||
action = ObjectChangeActionChoices.ACTION_CREATE
|
||||
@@ -65,6 +68,7 @@ def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
objectchange.save()
|
||||
|
||||
# If this is an M2M change, update the previously queued webhook (from post_save)
|
||||
webhook_queue = thread_locals.webhook_queue
|
||||
if m2m_changed and webhook_queue and is_same_object(instance, webhook_queue[-1]):
|
||||
instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments
|
||||
webhook_queue[-1]['data'] = serialize_for_webhook(instance)
|
||||
@@ -79,13 +83,15 @@ def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
model_updates.labels(instance._meta.model_name).inc()
|
||||
|
||||
|
||||
def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
def handle_deleted_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Fires when an object is deleted.
|
||||
"""
|
||||
if not hasattr(instance, 'to_objectchange'):
|
||||
return
|
||||
|
||||
request = get_request()
|
||||
|
||||
# Record an ObjectChange if applicable
|
||||
if hasattr(instance, 'to_objectchange'):
|
||||
objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
|
||||
@@ -94,19 +100,21 @@ def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
|
||||
objectchange.save()
|
||||
|
||||
# Enqueue webhooks
|
||||
webhook_queue = thread_locals.webhook_queue
|
||||
enqueue_object(webhook_queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
|
||||
# Increment metric counters
|
||||
model_deletes.labels(instance._meta.model_name).inc()
|
||||
|
||||
|
||||
def _clear_webhook_queue(webhook_queue, sender, **kwargs):
|
||||
def clear_webhook_queue(sender, **kwargs):
|
||||
"""
|
||||
Delete any queued webhooks (e.g. because of an aborted bulk transaction)
|
||||
"""
|
||||
logger = logging.getLogger('webhooks')
|
||||
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
|
||||
webhook_queue = thread_locals.webhook_queue
|
||||
|
||||
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
|
||||
webhook_queue.clear()
|
||||
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ class CustomFieldTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CustomField
|
||||
fields = (
|
||||
'pk', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default', 'description',
|
||||
'filter_logic', 'choices',
|
||||
'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default',
|
||||
'description', 'filter_logic', 'choices',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description')
|
||||
|
||||
@@ -78,7 +78,8 @@ class CustomLinkTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CustomLink
|
||||
fields = (
|
||||
'pk', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window',
|
||||
'pk', 'id', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name',
|
||||
'button_class', 'new_window',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window')
|
||||
|
||||
@@ -98,7 +99,7 @@ class ExportTemplateTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ExportTemplate
|
||||
fields = (
|
||||
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||
'pk', 'id', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||
@@ -132,7 +133,7 @@ class WebhookTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Webhook
|
||||
fields = (
|
||||
'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
|
||||
'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
|
||||
'payload_url', 'secret', 'ssl_validation', 'ca_file_path',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -155,10 +156,16 @@ class TagTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tag
|
||||
fields = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions')
|
||||
|
||||
|
||||
class TaggedItemTable(BaseTable):
|
||||
id = tables.Column(
|
||||
verbose_name='ID',
|
||||
linkify=lambda record: record.content_object.get_absolute_url(),
|
||||
accessor='content_object__id'
|
||||
)
|
||||
content_type = ContentTypeColumn(
|
||||
verbose_name='Type'
|
||||
)
|
||||
@@ -170,7 +177,7 @@ class TaggedItemTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = TaggedItem
|
||||
fields = ('content_type', 'content_object')
|
||||
fields = ('id', 'content_type', 'content_object')
|
||||
|
||||
|
||||
class ConfigContextTable(BaseTable):
|
||||
@@ -185,8 +192,8 @@ class ConfigContextTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConfigContext
|
||||
fields = (
|
||||
'pk', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', 'platforms',
|
||||
'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
|
||||
'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles',
|
||||
'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
|
||||
|
||||
@@ -211,7 +218,7 @@ class ObjectChangeTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ObjectChange
|
||||
fields = ('time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
|
||||
fields = ('id', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
|
||||
|
||||
|
||||
class ObjectJournalTable(BaseTable):
|
||||
@@ -232,7 +239,7 @@ class ObjectJournalTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = JournalEntry
|
||||
fields = ('created', 'created_by', 'kind', 'comments', 'actions')
|
||||
fields = ('id', 'created', 'created_by', 'kind', 'comments', 'actions')
|
||||
|
||||
|
||||
class JournalEntryTable(ObjectJournalTable):
|
||||
@@ -250,5 +257,10 @@ class JournalEntryTable(ObjectJournalTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = JournalEntry
|
||||
fields = (
|
||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments', 'actions'
|
||||
'pk', 'id', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind',
|
||||
'comments', 'actions'
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind',
|
||||
'comments', 'actions'
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import tempfile
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from netaddr import IPAddress, IPNetwork
|
||||
@@ -11,6 +13,50 @@ CHOICES = (
|
||||
('0000ff', 'Blue')
|
||||
)
|
||||
|
||||
YAML_DATA = """
|
||||
Foo: 123
|
||||
Bar: 456
|
||||
Baz:
|
||||
- A
|
||||
- B
|
||||
- C
|
||||
"""
|
||||
|
||||
JSON_DATA = """
|
||||
{
|
||||
"Foo": 123,
|
||||
"Bar": 456,
|
||||
"Baz": ["A", "B", "C"]
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class ScriptTest(TestCase):
|
||||
|
||||
def test_load_yaml(self):
|
||||
datafile = tempfile.NamedTemporaryFile()
|
||||
datafile.write(bytes(YAML_DATA, 'UTF-8'))
|
||||
datafile.seek(0)
|
||||
|
||||
data = Script().load_yaml(datafile.name)
|
||||
self.assertEqual(data, {
|
||||
'Foo': 123,
|
||||
'Bar': 456,
|
||||
'Baz': ['A', 'B', 'C'],
|
||||
})
|
||||
|
||||
def test_load_json(self):
|
||||
datafile = tempfile.NamedTemporaryFile()
|
||||
datafile.write(bytes(JSON_DATA, 'UTF-8'))
|
||||
datafile.seek(0)
|
||||
|
||||
data = Script().load_json(datafile.name)
|
||||
self.assertEqual(data, {
|
||||
'Foo': 123,
|
||||
'Bar': 456,
|
||||
'Baz': ['A', 'B', 'C'],
|
||||
})
|
||||
|
||||
|
||||
class ScriptVariablesTest(TestCase):
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from rq import Worker
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import copy_safe_request, count_related, shallow_compare_dict
|
||||
from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict
|
||||
from utilities.views import ContentTypePermissionRequiredMixin
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import JobResultStatusChoices
|
||||
@@ -475,11 +475,7 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
||||
def alter_obj(self, instance, request, args, kwargs):
|
||||
if not instance.pk:
|
||||
# Assign the parent object based on URL kwargs
|
||||
try:
|
||||
app_label, model = request.GET.get('content_type').split('.')
|
||||
except (AttributeError, ValueError):
|
||||
raise Http404("Content type not specified")
|
||||
content_type = get_object_or_404(ContentType, app_label=app_label, model=model)
|
||||
content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type'))
|
||||
instance.parent = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
|
||||
return instance
|
||||
|
||||
@@ -758,7 +754,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
|
||||
|
||||
def get(self, request, module, name):
|
||||
script = self._get_script(name, module)
|
||||
form = script.as_form(initial=request.GET)
|
||||
form = script.as_form(initial=normalize_querydict(request.GET))
|
||||
|
||||
# Look for a pending JobResult (use the latest one by creation timestamp)
|
||||
script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
||||
|
||||
@@ -257,11 +257,18 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
device = self.cleaned_data.get('device')
|
||||
virtual_machine = self.cleaned_data.get('virtual_machine')
|
||||
interface = self.cleaned_data.get('interface')
|
||||
is_primary = self.cleaned_data.get('is_primary')
|
||||
|
||||
# Validate is_primary
|
||||
if is_primary and not device and not virtual_machine:
|
||||
raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP")
|
||||
raise forms.ValidationError({
|
||||
"is_primary": "No device or virtual machine specified; cannot set as primary IP"
|
||||
})
|
||||
if is_primary and not interface:
|
||||
raise forms.ValidationError({
|
||||
"is_primary": "No interface specified; cannot set as primary IP"
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import django_filters
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -409,7 +410,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region_id', 'site_group_id', 'site_id'],
|
||||
['group_id', 'status', 'role_id'],
|
||||
['group_id', 'status', 'role_id', 'vid'],
|
||||
['tenant_group_id', 'tenant_id'],
|
||||
]
|
||||
q = forms.CharField(
|
||||
@@ -461,6 +462,10 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
||||
label=_('Role'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
vid = forms.IntegerField(
|
||||
required=False,
|
||||
label='VLAN ID'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class RIRTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ class AggregateTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Aggregate
|
||||
fields = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags')
|
||||
fields = ('pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags')
|
||||
default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class RoleTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Role
|
||||
fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'actions')
|
||||
default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -206,6 +206,11 @@ class PrefixTable(BaseTable):
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
vlan_group = tables.Column(
|
||||
accessor='vlan__group',
|
||||
linkify=True,
|
||||
verbose_name='VLAN Group'
|
||||
)
|
||||
vlan = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='VLAN'
|
||||
@@ -230,8 +235,8 @@ class PrefixTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = (
|
||||
'pk', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role',
|
||||
'is_pool', 'mark_utilized', 'description', 'tags',
|
||||
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan_group',
|
||||
'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
|
||||
@@ -260,11 +265,19 @@ class IPRangeTable(BaseTable):
|
||||
linkify=True
|
||||
)
|
||||
tenant = TenantColumn()
|
||||
utilization = UtilizationColumn(
|
||||
accessor='utilization',
|
||||
orderable=False
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='ipam:iprange_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPRange
|
||||
fields = (
|
||||
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
||||
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
||||
'utilization', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
||||
@@ -310,7 +323,7 @@ class IPAddressTable(BaseTable):
|
||||
verbose_name='NAT (Inside)'
|
||||
)
|
||||
assigned = BooleanColumn(
|
||||
accessor='assigned_object',
|
||||
accessor='assigned_object_id',
|
||||
linkify=True,
|
||||
verbose_name='Assigned'
|
||||
)
|
||||
@@ -321,7 +334,7 @@ class IPAddressTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description',
|
||||
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description',
|
||||
'tags',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -345,6 +358,7 @@ class IPAddressAssignTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'description')
|
||||
exclude = ('id', )
|
||||
orderable = False
|
||||
|
||||
|
||||
@@ -369,3 +383,4 @@ class InterfaceIPAddressTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'description')
|
||||
exclude = ('id', )
|
||||
|
||||
@@ -31,5 +31,5 @@ class ServiceTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Service
|
||||
fields = ('pk', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags')
|
||||
default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description')
|
||||
|
||||
@@ -81,7 +81,7 @@ class VLANGroupTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLANGroup
|
||||
fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ class VLANTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
vid = tables.TemplateColumn(
|
||||
template_code=VLAN_LINK,
|
||||
verbose_name='ID'
|
||||
verbose_name='VID'
|
||||
)
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
@@ -119,7 +119,7 @@ class VLANTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags')
|
||||
fields = ('pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags')
|
||||
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, VLAN) else '',
|
||||
@@ -149,6 +149,7 @@ class VLANDevicesTable(VLANMembersTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device', 'name', 'tagged', 'actions')
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class VLANVirtualMachinesTable(VLANMembersTable):
|
||||
@@ -160,6 +161,7 @@ class VLANVirtualMachinesTable(VLANMembersTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VMInterface
|
||||
fields = ('virtual_machine', 'name', 'tagged', 'actions')
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class InterfaceVLANTable(BaseTable):
|
||||
@@ -187,6 +189,7 @@ class InterfaceVLANTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('vid', 'tagged', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
|
||||
exclude = ('id', )
|
||||
|
||||
def __init__(self, interface, *args, **kwargs):
|
||||
self.interface = interface
|
||||
|
||||
@@ -47,7 +47,7 @@ class VRFTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VRF
|
||||
fields = (
|
||||
'pk', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tags',
|
||||
'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
|
||||
|
||||
@@ -68,5 +68,5 @@ class RouteTargetTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RouteTarget
|
||||
fields = ('pk', 'name', 'tenant', 'description', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags')
|
||||
default_columns = ('pk', 'name', 'tenant', 'description')
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import threading
|
||||
|
||||
thread_locals = threading.local()
|
||||
|
||||
@@ -34,7 +34,7 @@ class ObjectPermissionMixin():
|
||||
object_permissions = ObjectPermission.objects.filter(
|
||||
self.get_permission_filter(user_obj),
|
||||
enabled=True
|
||||
).prefetch_related('object_types')
|
||||
).order_by('id').distinct('id').prefetch_related('object_types')
|
||||
|
||||
# Create a dictionary mapping permissions to their constraints
|
||||
perms = defaultdict(list)
|
||||
|
||||
@@ -69,7 +69,13 @@ SEARCH_TYPES = OrderedDict((
|
||||
}),
|
||||
('location', {
|
||||
'queryset': Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import logging
|
||||
import uuid
|
||||
from urllib import parse
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import ProgrammingError
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
@@ -114,7 +114,7 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
|
||||
return groups
|
||||
|
||||
|
||||
class ObjectChangeMiddleware(object):
|
||||
class ObjectChangeMiddleware:
|
||||
"""
|
||||
This middleware performs three functions in response to an object being created, updated, or deleted:
|
||||
|
||||
|
||||
@@ -40,11 +40,6 @@ class ChangeLoggingMixin(models.Model):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
object_changes = GenericRelation(
|
||||
to='extras.ObjectChange',
|
||||
content_type_field='changed_object_type',
|
||||
object_id_field='changed_object_id'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
9
netbox/netbox/request_context.py
Normal file
9
netbox/netbox/request_context.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from netbox import thread_locals
|
||||
|
||||
|
||||
def set_request(request):
|
||||
thread_locals.request = request
|
||||
|
||||
|
||||
def get_request():
|
||||
return getattr(thread_locals, 'request', None)
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import platform
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import warnings
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@@ -16,7 +17,7 @@ from django.core.validators import URLValidator
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.0.7'
|
||||
VERSION = '3.0.11'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@@ -25,7 +26,7 @@ HOSTNAME = platform.node()
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Validate Python version
|
||||
if platform.python_version_tuple() < ('3', '7'):
|
||||
if sys.version_info < (3, 7):
|
||||
raise RuntimeError(
|
||||
f"NetBox requires Python 3.7 or higher (current: Python {platform.python_version()})"
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ class HomeView(View):
|
||||
release_version, release_url = latest_release
|
||||
if release_version > version.parse(settings.VERSION):
|
||||
new_release = {
|
||||
'version': str(latest_release),
|
||||
'version': str(release_version),
|
||||
'url': release_url,
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,13 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
||||
def get_required_permission(self):
|
||||
return get_permission_for_model(self.queryset.model, 'view')
|
||||
|
||||
def get_table(self, request, permissions):
|
||||
table = self.table(self.queryset, user=request.user)
|
||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||
table.columns.show('pk')
|
||||
|
||||
return table
|
||||
|
||||
def export_yaml(self):
|
||||
"""
|
||||
Export the queryset of objects as concatenated YAML documents.
|
||||
@@ -123,8 +130,20 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
||||
filename=f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv'
|
||||
)
|
||||
|
||||
def get(self, request):
|
||||
def export_template(self, template, request):
|
||||
"""
|
||||
Render an ExportTemplate using the current queryset.
|
||||
|
||||
:param template: ExportTemplate instance
|
||||
:param request: The current request
|
||||
"""
|
||||
try:
|
||||
return template.render_to_response(self.queryset)
|
||||
except Exception as e:
|
||||
messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}")
|
||||
return redirect(request.path)
|
||||
|
||||
def get(self, request):
|
||||
model = self.queryset.model
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
|
||||
@@ -137,42 +156,33 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
||||
perm_name = get_permission_for_model(model, action)
|
||||
permissions[action] = request.user.has_perm(perm_name)
|
||||
|
||||
# Export template/YAML rendering
|
||||
if 'export' in request.GET and request.GET['export'] != 'table':
|
||||
if 'export' in request.GET:
|
||||
|
||||
# An export template has been specified
|
||||
if request.GET['export']:
|
||||
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
||||
try:
|
||||
return et.render_to_response(self.queryset)
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
"There was an error rendering the selected export template ({}): {}".format(
|
||||
et.name, e
|
||||
)
|
||||
)
|
||||
# Export the current table view
|
||||
if request.GET['export'] == 'table':
|
||||
table = self.get_table(request, permissions)
|
||||
columns = [name for name, _ in table.selected_columns]
|
||||
return self.export_table(table, columns)
|
||||
|
||||
# Check for YAML export support
|
||||
# Render an ExportTemplate
|
||||
elif request.GET['export']:
|
||||
template = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
||||
return self.export_template(template, request)
|
||||
|
||||
# Check for YAML export support on the model
|
||||
elif hasattr(model, 'to_yaml'):
|
||||
response = HttpResponse(self.export_yaml(), content_type='text/yaml')
|
||||
filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
return response
|
||||
|
||||
# Construct the objects table
|
||||
table = self.table(self.queryset, user=request.user)
|
||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||
table.columns.show('pk')
|
||||
# Fall back to default table/YAML export
|
||||
else:
|
||||
table = self.get_table(request, permissions)
|
||||
return self.export_table(table)
|
||||
|
||||
# Handle table-based exports (current view or static CSV-based)
|
||||
if request.GET.get('export') == 'table':
|
||||
columns = [name for name, _ in table.selected_columns]
|
||||
return self.export_table(table, columns)
|
||||
elif 'export' in request.GET:
|
||||
return self.export_table(table)
|
||||
|
||||
# Paginate the objects table
|
||||
# Render the objects table
|
||||
table = self.get_table(request, permissions)
|
||||
paginate_table(table, request)
|
||||
|
||||
context = {
|
||||
@@ -282,11 +292,11 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
messages.success(request, mark_safe(msg))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
redirect_url = request.get_full_path()
|
||||
redirect_url = request.path
|
||||
|
||||
# If the object has clone_fields, pre-populate a new instance of the form
|
||||
if hasattr(obj, 'clone_fields'):
|
||||
redirect_url += f"{'&' if '?' in redirect_url else '?'}{prepare_cloned_fields(obj)}"
|
||||
redirect_url += f"?{prepare_cloned_fields(obj)}"
|
||||
|
||||
return redirect(redirect_url)
|
||||
|
||||
@@ -777,8 +787,21 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
else:
|
||||
pk_list = request.POST.getlist('pk')
|
||||
|
||||
# Include the PK list as initial data for the form
|
||||
initial_data = {'pk': pk_list}
|
||||
|
||||
# Check for other contextual data needed for the form. We avoid passing all of request.GET because the
|
||||
# filter values will conflict with the bulk edit form fields.
|
||||
# TODO: Find a better way to accomplish this
|
||||
if 'device' in request.GET:
|
||||
initial_data['device'] = request.GET.get('device')
|
||||
elif 'device_type' in request.GET:
|
||||
initial_data['device_type'] = request.GET.get('device_type')
|
||||
elif 'virtual_machine' in request.GET:
|
||||
initial_data['virtual_machine'] = request.GET.get('virtual_machine')
|
||||
|
||||
if '_apply' in request.POST:
|
||||
form = self.form(model, request.POST)
|
||||
form = self.form(model, request.POST, initial=initial_data)
|
||||
restrict_form_fields(form, request.user)
|
||||
|
||||
if form.is_valid():
|
||||
@@ -867,18 +890,6 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
logger.debug("Form validation failed")
|
||||
|
||||
else:
|
||||
# Include the PK list as initial data for the form
|
||||
initial_data = {'pk': pk_list}
|
||||
|
||||
# Check for other contextual data needed for the form. We avoid passing all of request.GET because the
|
||||
# filter values will conflict with the bulk edit form fields.
|
||||
# TODO: Find a better way to accomplish this
|
||||
if 'device' in request.GET:
|
||||
initial_data['device'] = request.GET.get('device')
|
||||
elif 'device_type' in request.GET:
|
||||
initial_data['device_type'] = request.GET.get('device_type')
|
||||
elif 'virtual_machine' in request.GET:
|
||||
initial_data['virtual_machine'] = request.GET.get('virtual_machine')
|
||||
|
||||
form = self.form(model, initial=initial_data)
|
||||
restrict_form_fields(form, request.user)
|
||||
|
||||
4
netbox/project-static/dist/lldp.js
vendored
4
netbox/project-static/dist/lldp.js
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/lldp.js.map
vendored
2
netbox/project-static/dist/lldp.js.map
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox-dark.css
vendored
2
netbox/project-static/dist/netbox-dark.css
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox-light.css
vendored
2
netbox/project-static/dist/netbox-light.css
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox-print.css
vendored
2
netbox/project-static/dist/netbox-print.css
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.js
vendored
2
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.js.map
vendored
2
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,17 @@
|
||||
import { createToast } from '../bs';
|
||||
import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
|
||||
|
||||
// Match an interface name that begins with a capital letter and is followed by at least one other
|
||||
// alphabetic character, and ends with a forward-slash-separated numeric sequence such as 0/1/2.
|
||||
const CISCO_IOS_PATTERN = new RegExp(/^([A-Z][A-Za-z]+)[^0-9]*([0-9/]+)$/);
|
||||
|
||||
// Mapping of overrides to default Cisco IOS interface alias behavior (default behavior is to use
|
||||
// the first two characters).
|
||||
const CISCO_IOS_OVERRIDES = new Map<string, string>([
|
||||
// Cisco IOS abbreviates 25G (TwentyFiveGigE) interfaces as 'Twe'.
|
||||
['TwentyFiveGigE', 'Twe'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Get an attribute from a row's cell.
|
||||
*
|
||||
@@ -12,6 +23,40 @@ function getData(row: HTMLTableRowElement, query: string, attr: string): string
|
||||
return row.querySelector(query)?.getAttribute(attr) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preconfigured alias for given interface. Primarily for matching long-form Cisco IOS
|
||||
* interface names with short-form Cisco IOS interface names. For example, `GigabitEthernet0/1/2`
|
||||
* would become `Gi0/1/2`.
|
||||
*
|
||||
* This should probably be replaced with something in the primary application (Django), such as
|
||||
* a database field attached to given interface types. However, this is a temporary measure to
|
||||
* replace the functionality of this one-liner:
|
||||
*
|
||||
* @see https://github.com/netbox-community/netbox/blob/9cc4992fad2fe04ef0211d998c517414e8871d8c/netbox/templates/dcim/device/lldp_neighbors.html#L69
|
||||
*
|
||||
* @param name Long-form/original interface name.
|
||||
*/
|
||||
function getInterfaceAlias(name: string | null): string | null {
|
||||
if (name === null) {
|
||||
return name;
|
||||
}
|
||||
if (name.match(CISCO_IOS_PATTERN)) {
|
||||
// Extract the base name and numeric portions of the interface. For example, an input interface
|
||||
// of `GigabitEthernet0/0/1` would result in an array of `['GigabitEthernet', '0/0/1']`.
|
||||
const [base, numeric] = (name.match(CISCO_IOS_PATTERN) ?? []).slice(1, 3);
|
||||
|
||||
if (isTruthy(base) && isTruthy(numeric)) {
|
||||
// Check the override map and use its value if the base name is present in the map.
|
||||
// Otherwise, use the first two characters of the base name. For example,
|
||||
// `GigabitEthernet0/0/1` would become `Gi0/0/1`, but `TwentyFiveGigE0/0/1` would become
|
||||
// `Twe0/0/1`.
|
||||
const aliasBase = CISCO_IOS_OVERRIDES.get(base) || base.slice(0, 2);
|
||||
return `${aliasBase}${numeric}`;
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update row styles based on LLDP neighbor data.
|
||||
*/
|
||||
@@ -23,38 +68,41 @@ function updateRowStyle(data: LLDPNeighborDetail) {
|
||||
|
||||
if (row !== null) {
|
||||
for (const neighbor of neighbors) {
|
||||
const cellDevice = row.querySelector<HTMLTableCellElement>('td.device');
|
||||
const cellInterface = row.querySelector<HTMLTableCellElement>('td.interface');
|
||||
const cDevice = getData(row, 'td.configured_device', 'data');
|
||||
const cChassis = getData(row, 'td.configured_chassis', 'data-chassis');
|
||||
const cInterface = getData(row, 'td.configured_interface', 'data');
|
||||
const deviceCell = row.querySelector<HTMLTableCellElement>('td.device');
|
||||
const interfaceCell = row.querySelector<HTMLTableCellElement>('td.interface');
|
||||
const configuredDevice = getData(row, 'td.configured_device', 'data');
|
||||
const configuredChassis = getData(row, 'td.configured_chassis', 'data-chassis');
|
||||
const configuredIface = getData(row, 'td.configured_interface', 'data');
|
||||
|
||||
let cInterfaceShort = null;
|
||||
if (isTruthy(cInterface)) {
|
||||
cInterfaceShort = cInterface.replace(/^([A-Z][a-z])[^0-9]*([0-9/]+)$/, '$1$2');
|
||||
const interfaceAlias = getInterfaceAlias(configuredIface);
|
||||
|
||||
const remoteName = neighbor.remote_system_name ?? '';
|
||||
const remotePort = neighbor.remote_port ?? '';
|
||||
const [neighborDevice] = remoteName.split('.');
|
||||
const [neighborIface] = remotePort.split('.');
|
||||
|
||||
if (deviceCell !== null) {
|
||||
deviceCell.innerText = neighborDevice;
|
||||
}
|
||||
|
||||
const nHost = neighbor.remote_system_name ?? '';
|
||||
const nPort = neighbor.remote_port ?? '';
|
||||
const [nDevice] = nHost.split('.');
|
||||
const [nInterface] = nPort.split('.');
|
||||
|
||||
if (cellDevice !== null) {
|
||||
cellDevice.innerText = nDevice;
|
||||
if (interfaceCell !== null) {
|
||||
interfaceCell.innerText = neighborIface;
|
||||
}
|
||||
|
||||
if (cellInterface !== null) {
|
||||
cellInterface.innerText = nInterface;
|
||||
}
|
||||
// Interface has an LLDP neighbor, but the neighbor is not configured in NetBox.
|
||||
const nonConfiguredDevice = !isTruthy(configuredDevice) && isTruthy(neighborDevice);
|
||||
|
||||
if (!isTruthy(cDevice) && isTruthy(nDevice)) {
|
||||
// NetBox device or chassis matches LLDP neighbor.
|
||||
const validNode =
|
||||
configuredDevice === neighborDevice || configuredChassis === neighborDevice;
|
||||
|
||||
// NetBox configured interface matches LLDP neighbor interface.
|
||||
const validInterface =
|
||||
configuredIface === neighborIface || interfaceAlias === neighborIface;
|
||||
|
||||
if (nonConfiguredDevice) {
|
||||
row.classList.add('info');
|
||||
} else if (
|
||||
(cDevice === nDevice || cChassis === nDevice) &&
|
||||
cInterfaceShort === nInterface
|
||||
) {
|
||||
row.classList.add('success');
|
||||
} else if (cDevice === nDevice || cChassis === nDevice) {
|
||||
} else if (validNode && validInterface) {
|
||||
row.classList.add('success');
|
||||
} else {
|
||||
row.classList.add('danger');
|
||||
|
||||
@@ -266,10 +266,8 @@ class SideNav {
|
||||
for (const link of this.getActiveLinks()) {
|
||||
this.activateLink(link, 'collapse');
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.bodyRemove('hide');
|
||||
this.bodyAdd('hidden');
|
||||
}, 300);
|
||||
this.bodyRemove('hide');
|
||||
this.bodyAdd('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,9 +197,15 @@ table {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.dropdown {
|
||||
// Presence of 'overflow: scroll' on a table causes dropdowns to be improperly hidden when
|
||||
// opened. See: https://github.com/twbs/bootstrap/issues/24251
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
th {
|
||||
a, a:hover {
|
||||
a,
|
||||
a:hover {
|
||||
color: $body-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -808,7 +814,7 @@ table .table-badge-group {
|
||||
}
|
||||
|
||||
&.badge:not(:last-of-type):not(:only-child) {
|
||||
margin-bottom: map.get($spacers, 2);
|
||||
margin-bottom: map.get($spacers, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,11 @@
|
||||
// Navbar brand
|
||||
.sidenav-brand {
|
||||
margin-right: 0;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.sidenav-brand-icon {
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.sidenav-inner {
|
||||
@@ -141,7 +146,17 @@
|
||||
}
|
||||
|
||||
.sidenav-toggle {
|
||||
display: none;
|
||||
// The sidenav toggle's default state is "hidden". Because modifying the `display` property
|
||||
// isn't ideal for smooth transitions, combine opacity 0 (transparent) and position absolute
|
||||
// to yield a similar result.
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
// The transition itself is largely irrelevant, but CSS needs *something* to transition in
|
||||
// order to apply a delay.
|
||||
transition: opacity 10ms ease-in-out;
|
||||
// Offset the transition delay so the icon isn't visible during the logo transition.
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
.sidenav-collapse {
|
||||
@@ -350,13 +365,21 @@
|
||||
.sidenav-brand {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transform: translateX(-150%);
|
||||
}
|
||||
|
||||
.sidenav-brand-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidenav-toggle {
|
||||
// Immediately hide the toggle when the sidenav is closed, so it doesn't linger and overlap
|
||||
// with the logo elements.
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
transition: unset;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
.navbar-nav > .nav-item {
|
||||
> .nav-link {
|
||||
&:after {
|
||||
@@ -402,7 +425,8 @@
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.sidenav-toggle {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ $btn-link-disabled-color: $gray-300;
|
||||
|
||||
// Forms
|
||||
$component-active-bg: $primary;
|
||||
$component-active-color: $black;
|
||||
$form-text-color: $text-muted;
|
||||
$input-bg: $gray-900;
|
||||
$input-disabled-bg: $gray-700;
|
||||
|
||||
@@ -27,55 +27,78 @@
|
||||
<title>{% block title %}Home{% endblock %} | NetBox</title>
|
||||
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Set the color mode on the `<html/>` element and in local storage.
|
||||
*/
|
||||
function setMode(mode) {
|
||||
document.documentElement.setAttribute("data-netbox-color-mode", mode);
|
||||
localStorage.setItem("netbox-color-mode", mode);
|
||||
}
|
||||
/**
|
||||
* Determine the best initial color mode to use prior to rendering.
|
||||
*/
|
||||
(function () {
|
||||
try {
|
||||
// Browser prefers dark color scheme.
|
||||
var preferDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
// Browser prefers light color scheme.
|
||||
var preferLight = window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||
// Client NetBox color-mode override.
|
||||
var clientMode = localStorage.getItem("netbox-color-mode");
|
||||
// NetBox server-rendered value.
|
||||
var serverMode = document.documentElement.getAttribute("data-netbox-color-mode");
|
||||
|
||||
if (clientMode === null && (serverMode === "light" || serverMode === "dark")) {
|
||||
// If the client mode is not set but the server mode is, use the server mode.
|
||||
return setMode(serverMode);
|
||||
}
|
||||
if (clientMode !== null && clientMode !== serverMode) {
|
||||
// If the client mode is set and is different than the server mode, use the client mode
|
||||
// over the server mode, as it should be more recent.
|
||||
return setMode(clientMode);
|
||||
}
|
||||
if (clientMode === serverMode) {
|
||||
// If the client and server modes match, use that value.
|
||||
return setMode(clientMode);
|
||||
}
|
||||
if (preferDark && serverMode === "unset") {
|
||||
// If the server mode is not set but the browser prefers dark mode, use dark mode.
|
||||
return setMode("dark");
|
||||
}
|
||||
if (preferLight && serverMode === "unset") {
|
||||
// If the server mode is not set but the browser prefers light mode, use light mode.
|
||||
return setMode("light");
|
||||
}
|
||||
} catch (error) {
|
||||
// In the event of an error, log it to the console and set the mode to light mode.
|
||||
console.error(error);
|
||||
}
|
||||
return setMode("light");
|
||||
})();
|
||||
|
||||
/**
|
||||
* Set the color mode on the `<html/>` element and in local storage.
|
||||
*
|
||||
* @param mode {"dark" | "light"} NetBox Color Mode.
|
||||
* @param inferred {boolean} Value is inferred from browser/system preference.
|
||||
*/
|
||||
function setMode(mode, inferred) {
|
||||
document.documentElement.setAttribute("data-netbox-color-mode", mode);
|
||||
localStorage.setItem("netbox-color-mode", mode);
|
||||
localStorage.setItem("netbox-color-mode-inferred", inferred);
|
||||
}
|
||||
/**
|
||||
* Determine the best initial color mode to use prior to rendering.
|
||||
*/
|
||||
(function () {
|
||||
try {
|
||||
// Browser prefers dark color scheme.
|
||||
var preferDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
// Browser prefers light color scheme.
|
||||
var preferLight = window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||
// Client NetBox color-mode override.
|
||||
var clientMode = localStorage.getItem("netbox-color-mode");
|
||||
// NetBox server-rendered value.
|
||||
var serverMode = document.documentElement.getAttribute("data-netbox-color-mode");
|
||||
// Color mode is inferred from browser/system preference and not deterministically set by
|
||||
// the client or server.
|
||||
var inferred = JSON.parse(localStorage.getItem("netbox-color-mode-inferred"));
|
||||
|
||||
if (inferred === true && (serverMode === "light" || serverMode === "dark")) {
|
||||
// The color mode was previously inferred from browser/system preference, but
|
||||
// the server now has a value, so we should use the server's value.
|
||||
return setMode(serverMode, false);
|
||||
}
|
||||
if (clientMode === null && (serverMode === "light" || serverMode === "dark")) {
|
||||
// If the client mode is not set but the server mode is, use the server mode.
|
||||
return setMode(serverMode, false);
|
||||
}
|
||||
if (clientMode !== null && serverMode === "unset") {
|
||||
// The color mode has been set, deterministically or otherwise, and the server
|
||||
// has no preference or has not been set. Use the client mode, but allow it to
|
||||
/// be overridden by the server if/when a server value exists.
|
||||
return setMode(clientMode, true);
|
||||
}
|
||||
if (
|
||||
clientMode !== null &&
|
||||
(serverMode === "light" || serverMode === "dark") &&
|
||||
clientMode !== serverMode
|
||||
) {
|
||||
// If the client mode is set and is different than the server mode (which is also set),
|
||||
// use the client mode over the server mode, as it should be more recent.
|
||||
return setMode(clientMode, false);
|
||||
}
|
||||
if (clientMode === serverMode) {
|
||||
// If the client and server modes match, use that value.
|
||||
return setMode(clientMode, false);
|
||||
}
|
||||
if (preferDark && serverMode === "unset") {
|
||||
// If the server mode is not set but the browser prefers dark mode, use dark mode, but
|
||||
// allow it to be overridden by an explicit preference.
|
||||
return setMode("dark", true);
|
||||
}
|
||||
if (preferLight && serverMode === "unset") {
|
||||
// If the server mode is not set but the browser prefers light mode, use light mode,
|
||||
// but allow it to be overridden by an explicit preference.
|
||||
return setMode("light", true);
|
||||
}
|
||||
} catch (error) {
|
||||
// In the event of an error, log it to the console and set the mode to light mode.
|
||||
console.error(error);
|
||||
}
|
||||
return setMode("light", true);
|
||||
})();
|
||||
</script>
|
||||
|
||||
{# Static resources #}
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
</div>
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-12">
|
||||
<ul class="nav nav-pills" role="tablist">
|
||||
<ul class="nav nav-pills mb-1" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-target="#interfaces" role="tab" data-bs-toggle="tab">
|
||||
Interfaces {% badge interface_table.rows|length %}
|
||||
|
||||
@@ -43,6 +43,13 @@
|
||||
<tr>
|
||||
<th scope="row">Racks</th>
|
||||
<td>
|
||||
{% if rack_count %}
|
||||
<div class="float-end noprint">
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?location_id={{ object.pk }}" class="btn btn-sm btn-primary" title="View elevations">
|
||||
<i class="mdi mdi-server"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ rack_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -219,8 +219,8 @@
|
||||
</tr>
|
||||
{% for location in locations %}
|
||||
<tr>
|
||||
<td style="padding-left: {{ location.level }}8px">
|
||||
<i class="mdi mdi-folder-open"></i>
|
||||
<td>
|
||||
{% for i in location.level|as_range %}<i class="mdi mdi-circle-small"></i>{% endfor %}
|
||||
<a href="{{ location.get_absolute_url }}">{{ location }}</a>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<a href="{{ vc_member.get_absolute_url }}">{{ vc_member }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% badge vc_member.vc_position %}
|
||||
{% badge vc_member.vc_position show_empty=True %}
|
||||
</td>
|
||||
<td>
|
||||
{% if object.master == vc_member %}
|
||||
|
||||
@@ -130,12 +130,12 @@
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% if object.postchange_data %}
|
||||
<pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %}
|
||||
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|render_json }}</span>
|
||||
{% endspaceless %}{% endfor %}
|
||||
</pre>
|
||||
<pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %}
|
||||
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|render_json }}</span>
|
||||
{% endspaceless %}{% endfor %}
|
||||
</pre>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,108 +3,94 @@
|
||||
|
||||
{% block title %}Reports{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-9">
|
||||
{% if reports %}
|
||||
{% for module, module_reports in reports %}
|
||||
<div class="card">
|
||||
<h5 class="card-header"><a name="module.{{ module }}"></a>{{ module|bettertitle }}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover table-headings reports">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end">Last Run</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for report in module_reports %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'extras:report' module=report.module name=report.class_name %}" id="{{ report.module }}.{{ report.class_name }}">{{ report.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% include 'extras/inc/job_label.html' with result=report.result %}
|
||||
</td>
|
||||
<td>{{ report.description|render_markdown|placeholder }}</td>
|
||||
<td class="text-end">
|
||||
{% if report.result %}
|
||||
<a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">{{ report.result.created|annotated_date }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Never</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if perms.extras.run_report %}
|
||||
<div class="float-end noprint">
|
||||
<form action="{% url 'extras:report' module=report.module name=report.class_name %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="_run" class="btn btn-primary btn-sm">
|
||||
{% if report.result %}
|
||||
<i class="mdi mdi-replay"></i> Run Again
|
||||
{% else %}
|
||||
<i class="mdi mdi-play"></i> Run Report
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% for method, stats in report.result.data.items %}
|
||||
<tr>
|
||||
<td colspan="4" class="method">
|
||||
{{ method }}
|
||||
</td>
|
||||
<td class="text-end text-nowrap report-stats">
|
||||
<span class="badge bg-success">{{ stats.success }}</span>
|
||||
<span class="badge bg-info">{{ stats.info }}</span>
|
||||
<span class="badge bg-warning">{{ stats.warning }}</span>
|
||||
<span class="badge bg-danger">{{ stats.failure }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" role="tab">Reports</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
<div class="tab-content">
|
||||
{% if reports %}
|
||||
{% for module, module_reports in reports %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
<a name="module.{{ module }}"></a>
|
||||
<i class="mdi mdi-file-document-outline"></i> {{ module|bettertitle }}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover table-headings reports">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="250">Name</th>
|
||||
<th width="110">Status</th>
|
||||
<th>Description</th>
|
||||
<th width="150" class="text-end">Last Run</th>
|
||||
<th width="120"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for report in module_reports %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'extras:report' module=report.module name=report.class_name %}" id="{{ report.module }}.{{ report.class_name }}">{{ report.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% include 'extras/inc/job_label.html' with result=report.result %}
|
||||
</td>
|
||||
<td>{{ report.description|render_markdown|placeholder }}</td>
|
||||
<td class="text-end">
|
||||
{% if report.result %}
|
||||
<a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">{{ report.result.created|annotated_date }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Never</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if perms.extras.run_report %}
|
||||
<div class="float-end noprint">
|
||||
<form action="{% url 'extras:report' module=report.module name=report.class_name %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="_run" class="btn btn-primary btn-sm" style="width: 110px">
|
||||
{% if report.result %}
|
||||
<i class="mdi mdi-replay"></i> Run Again
|
||||
{% else %}
|
||||
<i class="mdi mdi-play"></i> Run Report
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% for method, stats in report.result.data.items %}
|
||||
<tr>
|
||||
<td colspan="4" class="method">
|
||||
<span class="ps-3">{{ method }}</span>
|
||||
</td>
|
||||
<td class="text-end text-nowrap report-stats">
|
||||
<span class="badge bg-success">{{ stats.success }}</span>
|
||||
<span class="badge bg-info">{{ stats.info }}</span>
|
||||
<span class="badge bg-warning">{{ stats.warning }}</span>
|
||||
<span class="badge bg-danger">{{ stats.failure }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h4 class="alert-heading">No Reports Found</h4>
|
||||
Reports should be saved to <code>{{ settings.REPORTS_ROOT }}</code>.
|
||||
<hr/>
|
||||
<small>This path can be changed by setting <code>REPORTS_ROOT</code> in NetBox's configuration.</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
{% if reports %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% for module, module_reports in reports %}
|
||||
<h5>{{ module|bettertitle }}</h5>
|
||||
<div class="small mb-2">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for report in module_reports %}
|
||||
<a href="#{{ report.module }}.{{ report.class_name }}" class="list-group-item">
|
||||
<i class="mdi mdi-file-chart-outline"></i> {{ report.name }}
|
||||
<div class="float-end">
|
||||
{% include 'extras/inc/job_label.html' with result=report.result %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h4 class="alert-heading">No Reports Found</h4>
|
||||
Reports should be saved to <code>{{ settings.REPORTS_ROOT }}</code>.
|
||||
<hr/>
|
||||
<small>This path can be changed by setting <code>REPORTS_ROOT</code> in NetBox's configuration.</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content-wrapper %}
|
||||
|
||||
@@ -3,74 +3,66 @@
|
||||
|
||||
{% block title %}Scripts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-9">
|
||||
{% if scripts %}
|
||||
{% for module, module_scripts in scripts.items %}
|
||||
<h3><a name="module.{{ module }}"></a>{{ module|bettertitle }}</h3>
|
||||
<table class="table table-hover table-headings reports">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end">Last Run</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for class_name, script in module_scripts.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'extras:script' module=script.module name=class_name %}" name="script.{{ class_name }}">{{ script }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% include 'extras/inc/job_label.html' with result=script.result %}
|
||||
</td>
|
||||
<td>{{ script.Meta.description|render_markdown }}</td>
|
||||
{% if script.result %}
|
||||
<td class="text-end">
|
||||
<a href="{% url 'extras:script_result' job_result_pk=script.result.pk %}">{{ script.result.created|annotated_date }}</a>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="text-end text-muted">Never</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" role="tab">Scripts</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
<div class="tab-content">
|
||||
{% if scripts %}
|
||||
{% for module, module_scripts in scripts.items %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
<a name="module.{{ module }}"></a>
|
||||
<i class="mdi mdi-file-document-outline"></i> {{ module|bettertitle }}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover table-headings reports">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="250">Name</th>
|
||||
<th width="110">Status</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end">Last Run</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for class_name, script in module_scripts.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'extras:script' module=script.module name=class_name %}" name="script.{{ class_name }}">{{ script }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% include 'extras/inc/job_label.html' with result=script.result %}
|
||||
</td>
|
||||
<td>
|
||||
{{ script.Meta.description|render_markdown|placeholder }}
|
||||
</td>
|
||||
{% if script.result %}
|
||||
<td class="text-end">
|
||||
<a href="{% url 'extras:script_result' job_result_pk=script.result.pk %}">{{ script.result.created|annotated_date }}</a>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="text-end text-muted">Never</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<h4 class="alert-heading">No Scripts Found</h4>
|
||||
Scripts should be saved to <code>{{ settings.SCRIPTS_ROOT }}</code>.
|
||||
<hr/>
|
||||
This path can be changed by setting <code>SCRIPTS_ROOT</code> in NetBox's configuration.
|
||||
</div>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
{% if scripts %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% for module, module_scripts in scripts.items %}
|
||||
<h5>{{ module|bettertitle }}</h5>
|
||||
<div class="small mb-2">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for class_name, script in module_scripts.items %}
|
||||
<a href="#script.{{ class_name }}" class="list-group-item">
|
||||
<i class="mdi mdi-file-chart-outline"></i> {{ script.name }}
|
||||
<div class="float-end">
|
||||
{% include 'extras/inc/job_label.html' with result=script.result %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<h4 class="alert-heading">No Scripts Found</h4>
|
||||
Scripts should be saved to <code>{{ settings.SCRIPTS_ROOT }}</code>.
|
||||
<hr/>
|
||||
This path can be changed by setting <code>SCRIPTS_ROOT</code> in NetBox's configuration.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content-wrapper %}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<tr>
|
||||
<th scope="row">Update</th>
|
||||
<td>
|
||||
{% if object.type_create %}
|
||||
{% if object.type_update %}
|
||||
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||
@@ -57,7 +57,7 @@
|
||||
<tr>
|
||||
<th scope="row">Delete</th>
|
||||
<td>
|
||||
{% if object.type_create %}
|
||||
{% if object.type_delete %}
|
||||
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||
@@ -137,7 +137,11 @@
|
||||
Additional Headers
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<pre>{{ object.additional_headers }}</pre>
|
||||
{% if object.additional_headers %}
|
||||
<pre>{{ object.additional_headers }}</pre>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -145,7 +149,11 @@
|
||||
Body Template
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<pre>{{ object.body_template }}</pre>
|
||||
{% if object.body_template %}
|
||||
<pre>{{ object.body_template }}</pre>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
|
||||
@@ -6,9 +6,17 @@
|
||||
{% load plugins %}
|
||||
|
||||
{% block header %}
|
||||
{# Breadcrumbs #}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<div class="float-end">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
{# Breadcrumbs #}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
|
||||
{% endblock breadcrumbs %}
|
||||
</ol>
|
||||
</nav>
|
||||
{# Object identifier #}
|
||||
<div class="float-end px-3">
|
||||
<code class="text-muted">
|
||||
{% block object_identifier %}
|
||||
{{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
|
||||
@@ -16,12 +24,7 @@
|
||||
{% endblock object_identifier %}
|
||||
</code>
|
||||
</div>
|
||||
<ol class="breadcrumb">
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
|
||||
{% endblock breadcrumbs %}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<table class="table table-hover attr-table">
|
||||
{% for field, value in custom_fields.items %}
|
||||
<tr>
|
||||
<td><span title="{{ field.description }}">{{ field }}</span></td>
|
||||
<td><span title="{{ field.description|escape }}">{{ field }}</span></td>
|
||||
<td>
|
||||
{% if field.type == 'boolean' and value == True %}
|
||||
<i class="mdi mdi-check-bold text-success" title="True"></i>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
{% if perms.extras.add_imageattachment %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
|
||||
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Attach an image
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,41 +1,43 @@
|
||||
{% load django_tables2 %}
|
||||
|
||||
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
|
||||
<div class="table-responsive">
|
||||
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
|
||||
{% if table.show_header %}
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
{% if column.orderable %}
|
||||
<th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
|
||||
{% else %}
|
||||
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
{% if column.orderable %}
|
||||
<th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
|
||||
{% else %}
|
||||
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% endif %}
|
||||
<tbody>
|
||||
{% for row in table.page.object_list|default:table.rows %}
|
||||
<tr {{ row.attrs.as_html }}>
|
||||
{% for column, cell in row.items %}
|
||||
<td {{ column.attrs.td.as_html }}>{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
{% if table.empty_text %}
|
||||
<tr>
|
||||
<td colspan="{{ table.columns|length }}" class="text-center text-muted">— {{ table.empty_text }} —</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for row in table.page.object_list|default:table.rows %}
|
||||
<tr {{ row.attrs.as_html }}>
|
||||
{% for column, cell in row.items %}
|
||||
<td {{ column.attrs.td.as_html }}>{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
{% if table.empty_text %}
|
||||
<tr>
|
||||
<td colspan="{{ table.columns|length }}" class="text-center text-muted">— {{ table.empty_text }} —</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% if table.has_footer %}
|
||||
<tfoot>
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
<td>{{ column.footer }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tfoot>
|
||||
<tfoot>
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
<td>{{ column.footer }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@@ -68,7 +69,7 @@
|
||||
VLANs
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% include 'inc/table.html' with table=vlans_table %}
|
||||
{% render_table vlans_table 'inc/table.html' %}
|
||||
</div>
|
||||
{% if perms.ipam.add_vlan %}
|
||||
<div class="card-footer text-end noprint">
|
||||
|
||||
@@ -55,7 +55,7 @@ class TenantGroupTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = TenantGroup
|
||||
fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'tenant_count', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -78,5 +78,5 @@ class TenantTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tenant
|
||||
fields = ('pk', 'name', 'slug', 'group', 'description', 'comments', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'group', 'description', 'comments', 'tags')
|
||||
default_columns = ('pk', 'name', 'group', 'description')
|
||||
|
||||
@@ -99,8 +99,20 @@ class TokenFilterSet(BaseFilterSet):
|
||||
model = Token
|
||||
fields = ['id', 'key', 'write_enabled']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(user__username__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ObjectPermissionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='users',
|
||||
queryset=User.objects.all(),
|
||||
@@ -127,3 +139,11 @@ class ObjectPermissionFilterSet(BaseFilterSet):
|
||||
class Meta:
|
||||
model = ObjectPermission
|
||||
fields = ['id', 'name', 'enabled', 'object_types']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ class GroupType(DjangoObjectType):
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
return RestrictedQuerySet(model=Group)
|
||||
return RestrictedQuerySet(model=Group).restrict(info.context.user, 'view')
|
||||
|
||||
|
||||
class UserType(DjangoObjectType):
|
||||
@@ -34,4 +34,4 @@ class UserType(DjangoObjectType):
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
return RestrictedQuerySet(model=User)
|
||||
return RestrictedQuerySet(model=User).restrict(info.context.user, 'view')
|
||||
|
||||
@@ -224,7 +224,7 @@ class CSVFileField(forms.FileField):
|
||||
return None
|
||||
|
||||
csv_str = file.read().decode('utf-8').strip()
|
||||
reader = csv.reader(csv_str.splitlines())
|
||||
reader = csv.reader(StringIO(csv_str))
|
||||
headers, records = parse_csv(reader)
|
||||
|
||||
return headers, records
|
||||
@@ -304,7 +304,7 @@ class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField):
|
||||
app_label, model = name.split('.')
|
||||
ct_filter |= Q(app_label=app_label, model=model)
|
||||
return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True))
|
||||
return super().prepare_value(value)
|
||||
return f'{value.app_label}.{value.model}'
|
||||
|
||||
|
||||
#
|
||||
|
||||
16
netbox/utilities/markdown.py
Normal file
16
netbox/utilities/markdown.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import markdown
|
||||
from markdown.inlinepatterns import SimpleTagPattern
|
||||
|
||||
STRIKE_RE = r'(~{2})(.+?)(~{2})'
|
||||
|
||||
|
||||
class StrikethroughExtension(markdown.Extension):
|
||||
"""
|
||||
A python-markdown extension which support strikethrough formatting (e.g. "~~text~~").
|
||||
"""
|
||||
def extendMarkdown(self, md):
|
||||
md.inlinePatterns.register(
|
||||
markdown.inlinepatterns.SimpleTagPattern(STRIKE_RE, 'del'),
|
||||
'strikethrough',
|
||||
200
|
||||
)
|
||||
@@ -57,17 +57,22 @@ def get_paginate_count(request):
|
||||
|
||||
Return the lesser of the calculated value and MAX_PAGE_SIZE.
|
||||
"""
|
||||
def _max_allowed(page_size):
|
||||
if settings.MAX_PAGE_SIZE:
|
||||
return min(page_size, settings.MAX_PAGE_SIZE)
|
||||
return page_size
|
||||
|
||||
if 'per_page' in request.GET:
|
||||
try:
|
||||
per_page = int(request.GET.get('per_page'))
|
||||
if request.user.is_authenticated:
|
||||
request.user.config.set('pagination.per_page', per_page, commit=True)
|
||||
return min(per_page, settings.MAX_PAGE_SIZE)
|
||||
return _max_allowed(per_page)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if request.user.is_authenticated:
|
||||
per_page = request.user.config.get('pagination.per_page', settings.PAGINATE_COUNT)
|
||||
return min(per_page, settings.MAX_PAGE_SIZE)
|
||||
return _max_allowed(per_page)
|
||||
|
||||
return min(settings.PAGINATE_COUNT, settings.MAX_PAGE_SIZE)
|
||||
return _max_allowed(settings.PAGINATE_COUNT)
|
||||
|
||||
@@ -23,6 +23,10 @@ class BaseTable(tables.Table):
|
||||
|
||||
:param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
|
||||
"""
|
||||
id = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='ID'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Dict, Any
|
||||
import yaml
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.defaultfilters import date
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils import timezone
|
||||
@@ -14,6 +15,7 @@ from django.utils.safestring import mark_safe
|
||||
from markdown import markdown
|
||||
|
||||
from utilities.forms import get_selected_values, TableConfigForm
|
||||
from utilities.markdown import StrikethroughExtension
|
||||
from utilities.utils import foreground_color
|
||||
|
||||
register = template.Library()
|
||||
@@ -39,16 +41,21 @@ def render_markdown(value):
|
||||
"""
|
||||
Render text as Markdown
|
||||
"""
|
||||
schemes = '|'.join(settings.ALLOWED_URL_SCHEMES)
|
||||
|
||||
# Strip HTML tags
|
||||
value = strip_tags(value)
|
||||
|
||||
# Sanitize Markdown links
|
||||
schemes = '|'.join(settings.ALLOWED_URL_SCHEMES)
|
||||
pattern = fr'\[(.+)\]\((?!({schemes})).*:(.+)\)'
|
||||
pattern = fr'\[([^\]]+)\]\((?!({schemes})).*:(.+)\)'
|
||||
value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE)
|
||||
|
||||
# Sanitize Markdown reference links
|
||||
pattern = fr'\[(.+)\]:\w?(?!({schemes})).*:(.+)'
|
||||
value = re.sub(pattern, '[\\1]: \\3', value, flags=re.IGNORECASE)
|
||||
|
||||
# Render Markdown
|
||||
html = markdown(value, extensions=['fenced_code', 'tables'])
|
||||
html = markdown(value, extensions=['fenced_code', 'tables', StrikethroughExtension()])
|
||||
|
||||
return mark_safe(html)
|
||||
|
||||
@@ -58,7 +65,7 @@ def render_json(value):
|
||||
"""
|
||||
Render a dictionary as formatted JSON.
|
||||
"""
|
||||
return json.dumps(value, indent=4, sort_keys=True)
|
||||
return json.dumps(value, ensure_ascii=False, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
@register.filter()
|
||||
@@ -78,6 +85,25 @@ def meta(obj, attr):
|
||||
return getattr(obj._meta, attr, '')
|
||||
|
||||
|
||||
@register.filter()
|
||||
def content_type(obj):
|
||||
"""
|
||||
Return the ContentType for the given object.
|
||||
"""
|
||||
return ContentType.objects.get_for_model(obj)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def content_type_id(obj):
|
||||
"""
|
||||
Return the ContentType ID for the given object.
|
||||
"""
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
if content_type:
|
||||
return content_type.pk
|
||||
return None
|
||||
|
||||
|
||||
@register.filter()
|
||||
def viewname(model, action):
|
||||
"""
|
||||
|
||||
@@ -345,7 +345,7 @@ class APIViewTestCases:
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
|
||||
|
||||
id_list = self._get_queryset().values_list('id', flat=True)[:3]
|
||||
id_list = list(self._get_queryset().values_list('id', flat=True)[:3])
|
||||
self.assertEqual(len(id_list), 3, "Insufficient number of objects to test bulk update")
|
||||
data = [
|
||||
{'id': id, **self.bulk_update_data} for id in id_list
|
||||
@@ -416,7 +416,7 @@ class APIViewTestCases:
|
||||
|
||||
# Target the three most recently created objects to avoid triggering recursive deletions
|
||||
# (e.g. with MPTT objects)
|
||||
id_list = self._get_queryset().order_by('-id').values_list('id', flat=True)[:3]
|
||||
id_list = list(self._get_queryset().order_by('-id').values_list('id', flat=True)[:3])
|
||||
self.assertEqual(len(id_list), 3, "Insufficient number of objects to test bulk deletion")
|
||||
data = [{"id": id} for id in id_list]
|
||||
|
||||
|
||||
@@ -327,13 +327,6 @@ def decode_dict(encoded_dict: Dict, *, decode_keys: bool = True) -> Dict:
|
||||
return {urllib.parse.unquote(k): decode_value(v, decode_keys) for k, v in encoded_dict.items()}
|
||||
|
||||
|
||||
# Taken from django.utils.functional (<3.0)
|
||||
def curry(_curried_func, *args, **kwargs):
|
||||
def _curried(*moreargs, **morekwargs):
|
||||
return _curried_func(*args, *moreargs, **{**kwargs, **morekwargs})
|
||||
return _curried
|
||||
|
||||
|
||||
def array_to_string(array):
|
||||
"""
|
||||
Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.
|
||||
|
||||
@@ -144,6 +144,12 @@ class VirtualMachineFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConf
|
||||
queryset=Cluster.objects.all(),
|
||||
label='Cluster (ID)',
|
||||
)
|
||||
cluster = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster__name',
|
||||
queryset=Cluster.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Cluster',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='cluster__site__region',
|
||||
|
||||
@@ -44,7 +44,7 @@ class ClusterTypeTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ClusterType
|
||||
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class ClusterGroupTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ClusterGroup
|
||||
fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions')
|
||||
fields = ('pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class ClusterTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Cluster
|
||||
fields = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'tags')
|
||||
fields = ('pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class VirtualMachineTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualMachine
|
||||
fields = (
|
||||
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'primary_ip4',
|
||||
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'primary_ip4',
|
||||
'primary_ip6', 'primary_ip', 'comments', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
@@ -170,7 +170,7 @@ class VMInterfaceTable(BaseInterfaceTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VMInterface
|
||||
fields = (
|
||||
'pk', 'name', 'virtual_machine', 'enabled', 'parent', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'parent', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||
'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'parent', 'description')
|
||||
@@ -186,7 +186,7 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VMInterface
|
||||
fields = (
|
||||
'pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'ip_addresses',
|
||||
'pk', 'id', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user