Compare commits
353 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfda5d9011 | ||
|
|
62a80c46a8 | ||
|
|
ceec1055e0 | ||
|
|
540bba4544 | ||
|
|
44c248e6c2 | ||
|
|
3a62fd49e6 | ||
|
|
a2007a4728 | ||
|
|
316c3808f7 | ||
|
|
928d880f0e | ||
|
|
c6930e3ea8 | ||
|
|
564884a774 | ||
|
|
7401fd7050 | ||
|
|
4a95cfd1c4 | ||
|
|
cd8943144b | ||
|
|
8400509358 | ||
|
|
d971131198 | ||
|
|
5729a06348 | ||
|
|
d59d23e308 | ||
|
|
3d1501e8fd | ||
|
|
c854c29016 | ||
|
|
33d8f8e5e7 | ||
|
|
93e241e8f3 | ||
|
|
43da786016 | ||
|
|
271d524687 | ||
|
|
4ebcdd2b8f | ||
|
|
2af8891f70 | ||
|
|
4e39021b6f | ||
|
|
2cd5fce62d | ||
|
|
ade307bc03 | ||
|
|
c8be4ef8e2 | ||
|
|
816214361d | ||
|
|
d1970ca85b | ||
|
|
8001694a4c | ||
|
|
10e258739f | ||
|
|
f3fdf03661 | ||
|
|
44814f759c | ||
|
|
4f5caa5ed2 | ||
|
|
aa7f04bf1b | ||
|
|
aaf1ea52b7 | ||
|
|
7990cfb078 | ||
|
|
a25ee66150 | ||
|
|
867af61875 | ||
|
|
8f4fa065f9 | ||
|
|
edb5220228 | ||
|
|
18332bdbf1 | ||
|
|
f1a7bceef2 | ||
|
|
eac2ace80b | ||
|
|
174ba6cf0f | ||
|
|
658c9347f3 | ||
|
|
7b3ef2ade5 | ||
|
|
2a62b628cf | ||
|
|
d8c07abd68 | ||
|
|
8d486c5838 | ||
|
|
eb91934d70 | ||
|
|
01654765e8 | ||
|
|
4c504870e0 | ||
|
|
3d687a6c2d | ||
|
|
96c4696417 | ||
|
|
e7659a5f99 | ||
|
|
53c9c3cf8d | ||
|
|
f60312febf | ||
|
|
7505baf3a1 | ||
|
|
33c6142365 | ||
|
|
10e874039f | ||
|
|
060ee2dd96 | ||
|
|
43d1182b4b | ||
|
|
d53da57f63 | ||
|
|
028b4b7ea7 | ||
|
|
4cb0230878 | ||
|
|
2fe8df3cbb | ||
|
|
64d67e3b00 | ||
|
|
aaf829898b | ||
|
|
8481cf66e3 | ||
|
|
bb150379a2 | ||
|
|
cc811e5a56 | ||
|
|
a9e583a693 | ||
|
|
3a3ff474cb | ||
|
|
cc00789d35 | ||
|
|
689f11a573 | ||
|
|
ae90ad1fb7 | ||
|
|
56d9725c39 | ||
|
|
1c69bfaf2c | ||
|
|
5e37f82b2f | ||
|
|
bdefd8ea8c | ||
|
|
eabd405845 | ||
|
|
03946f2ca8 | ||
|
|
fec8d1bc2f | ||
|
|
53f5f46037 | ||
|
|
b227757b9a | ||
|
|
eef5cefb5d | ||
|
|
7712b81ab9 | ||
|
|
7feb86fe55 | ||
|
|
d1efbf6620 | ||
|
|
aabee05a6a | ||
|
|
cf062b5b6a | ||
|
|
0b6a3898fe | ||
|
|
517ebcfbcd | ||
|
|
9ef24d3f43 | ||
|
|
02ffc2ddee | ||
|
|
62820ea2b8 | ||
|
|
04738587e8 | ||
|
|
cbbfcd0e7b | ||
|
|
309a70df89 | ||
|
|
4cb6984a65 | ||
|
|
3c32c09a5a | ||
|
|
2d9852d6f1 | ||
|
|
05542324fc | ||
|
|
669e86f96e | ||
|
|
cbf928f363 | ||
|
|
43b18c13e3 | ||
|
|
dda193247a | ||
|
|
2463e4efd3 | ||
|
|
a0b17887fd | ||
|
|
96784640e3 | ||
|
|
b75d12fe05 | ||
|
|
5e389c32ed | ||
|
|
fd89ef04b6 | ||
|
|
abcc10e938 | ||
|
|
3ad337dd15 | ||
|
|
a527767caa | ||
|
|
39129ecedf | ||
|
|
c97d2d4fe9 | ||
|
|
7735634649 | ||
|
|
148c6a6c23 | ||
|
|
360172cad0 | ||
|
|
75c91232b4 | ||
|
|
0190c0225e | ||
|
|
86d366be4d | ||
|
|
71d71a6b1b | ||
|
|
695ad47fe9 | ||
|
|
1b62c11db5 | ||
|
|
83a66a672d | ||
|
|
30b9ddc251 | ||
|
|
4a9831bd23 | ||
|
|
59388d89a0 | ||
|
|
1d033bd286 | ||
|
|
935f008c16 | ||
|
|
b5d57f3418 | ||
|
|
05b17a0082 | ||
|
|
91fd25a548 | ||
|
|
e05696dfcc | ||
|
|
157a45b627 | ||
|
|
e9a91455e8 | ||
|
|
f97eb99950 | ||
|
|
c4b7ab067a | ||
|
|
7477f6584e | ||
|
|
7b4f5252f1 | ||
|
|
4e03419e85 | ||
|
|
4d97043e26 | ||
|
|
c335b76ec6 | ||
|
|
356ff457be | ||
|
|
833c3fbd39 | ||
|
|
ace66eab61 | ||
|
|
ea9d2e3f88 | ||
|
|
211a1394d3 | ||
|
|
44032ffc11 | ||
|
|
f10460d774 | ||
|
|
57365ef7b9 | ||
|
|
d24f10ce6e | ||
|
|
1daa2ff98d | ||
|
|
0e6a6b784d | ||
|
|
e3576e2614 | ||
|
|
0899d7aefd | ||
|
|
d078befd33 | ||
|
|
f67cb71dbc | ||
|
|
721cd578bb | ||
|
|
3aac62caa7 | ||
|
|
6b9eb57de7 | ||
|
|
2b2a41edd2 | ||
|
|
cd1ad452da | ||
|
|
77868a9b17 | ||
|
|
59a2a43473 | ||
|
|
385a0f979e | ||
|
|
770cc5a700 | ||
|
|
a03155432e | ||
|
|
d51e833bf3 | ||
|
|
c6644ec1ae | ||
|
|
a7562a6aa1 | ||
|
|
6b70436e2b | ||
|
|
3dbc7bdd2c | ||
|
|
b4877e7fac | ||
|
|
f489ffa043 | ||
|
|
211c7641c1 | ||
|
|
51d066a1bc | ||
|
|
b702822857 | ||
|
|
1cbb2320c1 | ||
|
|
1c4ad3f817 | ||
|
|
ef5ec06141 | ||
|
|
0c37236d60 | ||
|
|
def853e8c4 | ||
|
|
7826cfb01f | ||
|
|
ac8f0a7ef2 | ||
|
|
48a907ae45 | ||
|
|
2fe620df70 | ||
|
|
536bd37d05 | ||
|
|
d29d265b0a | ||
|
|
41d653738a | ||
|
|
1955497dbe | ||
|
|
572803d7ac | ||
|
|
25a474f9f7 | ||
|
|
c42f7ab6d3 | ||
|
|
a7717b432e | ||
|
|
2acfda3dc5 | ||
|
|
569525fb68 | ||
|
|
15353b7513 | ||
|
|
4737336b85 | ||
|
|
d85b1c775f | ||
|
|
2ce1a96468 | ||
|
|
239b8d2e7c | ||
|
|
43b3ce9ed1 | ||
|
|
80d5a966db | ||
|
|
b11fa53519 | ||
|
|
ce4d00dc21 | ||
|
|
bb269affe2 | ||
|
|
d818c250b0 | ||
|
|
3d4d880110 | ||
|
|
899b61264f | ||
|
|
4dc059fba3 | ||
|
|
f94792fad8 | ||
|
|
6df2ff7ebf | ||
|
|
dc4ddedca3 | ||
|
|
73b85f9b29 | ||
|
|
ce054dd37d | ||
|
|
3a0e91a688 | ||
|
|
60ca4f29d7 | ||
|
|
301ebe0da3 | ||
|
|
ada745324f | ||
|
|
a69eec5fb9 | ||
|
|
8c35ebbb7e | ||
|
|
5ef2d1d7ad | ||
|
|
815b2d8a2b | ||
|
|
0cfe2d882d | ||
|
|
6019b738a4 | ||
|
|
de17a651e6 | ||
|
|
91dc9f0c9d | ||
|
|
0770aa237a | ||
|
|
d3f2d77961 | ||
|
|
da03b22fe4 | ||
|
|
456647838c | ||
|
|
e192ac34d0 | ||
|
|
6a3ccda12e | ||
|
|
14f79ef85a | ||
|
|
924471ee76 | ||
|
|
cd5844b050 | ||
|
|
0f6a12b595 | ||
|
|
d19c6a6afc | ||
|
|
3257ea00b5 | ||
|
|
f073087379 | ||
|
|
1ba47ae67c | ||
|
|
8ca3dfd8c9 | ||
|
|
cdea30253b | ||
|
|
c8ee01ba0f | ||
|
|
3ad08c75c0 | ||
|
|
c12e545ccd | ||
|
|
6571faad6c | ||
|
|
f98f1647da | ||
|
|
249b8b0363 | ||
|
|
777af35030 | ||
|
|
23fafe1996 | ||
|
|
e0741cc9af | ||
|
|
0e4911a575 | ||
|
|
28bc76695a | ||
|
|
4f7287fec5 | ||
|
|
0b5478ad2d | ||
|
|
cd6911f83c | ||
|
|
173f27cb64 | ||
|
|
152dcbe522 | ||
|
|
2a7bad326d | ||
|
|
cb6d8bf063 | ||
|
|
83db8d2072 | ||
|
|
3da8e4c1bb | ||
|
|
ee111a28d4 | ||
|
|
d00d9cb00f | ||
|
|
74ba5a61bf | ||
|
|
fd81f57e61 | ||
|
|
e75e189933 | ||
|
|
88d2fca2c6 | ||
|
|
bd615ebf65 | ||
|
|
44b9e822d9 | ||
|
|
ed4fe6bd36 | ||
|
|
a416ff6314 | ||
|
|
61ba5817c9 | ||
|
|
7bdbdda7f9 | ||
|
|
a9ddd41729 | ||
|
|
1379b9c9fb | ||
|
|
32615befd5 | ||
|
|
7697779abf | ||
|
|
bb37ebf4ba | ||
|
|
5f37699736 | ||
|
|
9da9a209a5 | ||
|
|
482b4b6e95 | ||
|
|
ec2e8ad184 | ||
|
|
6c686af1b7 | ||
|
|
4132027ada | ||
|
|
f70ef7a585 | ||
|
|
eb2bf3469e | ||
|
|
bfbf97aec9 | ||
|
|
2baf06e012 | ||
|
|
c2c8bd0a76 | ||
|
|
36729fb6ae | ||
|
|
18d5576997 | ||
|
|
6a7c56d919 | ||
|
|
3110765d12 | ||
|
|
deee36651d | ||
|
|
1c46102c4a | ||
|
|
439cf1a308 | ||
|
|
219f2eee29 | ||
|
|
374abe5214 | ||
|
|
8b1a462a60 | ||
|
|
c11ca543e2 | ||
|
|
bf92e3a9dd | ||
|
|
7ba0b420f1 | ||
|
|
f3906dd7c4 | ||
|
|
984d8b8ee6 | ||
|
|
7b4189271c | ||
|
|
63e8faeed9 | ||
|
|
f48aaf1c46 | ||
|
|
41499b189c | ||
|
|
9fddd193b9 | ||
|
|
0c7c61b685 | ||
|
|
6179686c81 | ||
|
|
2bb79e1346 | ||
|
|
71bf5f4697 | ||
|
|
069c2d2fd2 | ||
|
|
f35ff105ab | ||
|
|
25ec624e4e | ||
|
|
a972174706 | ||
|
|
646272f9b3 | ||
|
|
743106e94f | ||
|
|
f5d81f51c4 | ||
|
|
e8f62eb1f9 | ||
|
|
ea1467add7 | ||
|
|
917439725a | ||
|
|
2ef9e2d6fc | ||
|
|
c14a5973c7 | ||
|
|
3a7ea62874 | ||
|
|
0a38c16cc2 | ||
|
|
c65a291698 | ||
|
|
38a8ddcd77 | ||
|
|
43ad8e80b9 | ||
|
|
928dff6b68 | ||
|
|
0bdee1d6d8 | ||
|
|
3f40e15ed5 | ||
|
|
a687aa1de6 | ||
|
|
c811eb069d | ||
|
|
eb3d3dcbc4 | ||
|
|
804c064a7e | ||
|
|
9059c09627 | ||
|
|
279253c486 | ||
|
|
c7d6fe2d62 | ||
|
|
5327857f81 | ||
|
|
3b4dd051f2 | ||
|
|
587a34442a |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.3.0
|
||||
placeholder: v3.3.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/documentation_change.yaml
vendored
@@ -19,11 +19,15 @@ body:
|
||||
label: Area
|
||||
description: To what section of the documentation does this change primarily pertain?
|
||||
options:
|
||||
- Installation instructions
|
||||
- Configuration parameters
|
||||
- Functionality/features
|
||||
- REST API
|
||||
- Administration/development
|
||||
- Features
|
||||
- Installation/upgrade
|
||||
- Getting started
|
||||
- Configuration
|
||||
- Customization
|
||||
- Integrations/API
|
||||
- Plugins
|
||||
- Administration
|
||||
- Development
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
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.3.0
|
||||
placeholder: v3.3.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
20
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,15 +1,17 @@
|
||||
<!--
|
||||
Thank you for your interest in contributing to NetBox! Please note
|
||||
that our contribution policy requires that a feature request or bug
|
||||
report be opened for approval prior to filing a pull request. This
|
||||
helps avoid wasting time and effort on something that we might not
|
||||
be able to accept.
|
||||
Thank you for your interest in contributing to NetBox! Please note that
|
||||
our contribution policy requires that a feature request or bug report be
|
||||
approved and assigned prior to opening a pull request. This helps avoid
|
||||
waste time and effort on a proposed change that we might not be able to
|
||||
accept.
|
||||
|
||||
Please indicate the relevant feature request or bug report below.
|
||||
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
|
||||
FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
|
||||
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
|
||||
TO YOU, IT WILL BE CLOSED AUTOMATICALLY.
|
||||
|
||||
Please specify your assigned issue number on the line below.
|
||||
-->
|
||||
### Fixes: <ISSUE NUMBER GOES HERE>
|
||||
### Fixes: #1234
|
||||
|
||||
<!--
|
||||
Please include a summary of the proposed changes below.
|
||||
-->
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
@@ -1,5 +1,7 @@
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
16
.github/workflows/lock.yml
vendored
@@ -4,18 +4,18 @@ name: 'Lock threads'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: '90'
|
||||
issue-exclude-created-before: ''
|
||||
issue-exclude-labels: ''
|
||||
issue-lock-labels: ''
|
||||
issue-lock-comment: ''
|
||||
issue-inactive-days: 90
|
||||
pr-inactive-days: 30
|
||||
issue-lock-reason: 'resolved'
|
||||
process-only: 'issues'
|
||||
|
||||
9
.github/workflows/stale.yml
vendored
@@ -1,14 +1,21 @@
|
||||
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
|
||||
name: 'Close stale issues/PRs'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
- uses: actions/stale@v6
|
||||
with:
|
||||
close-issue-message: >
|
||||
This issue has been automatically closed due to lack of activity. In an
|
||||
|
||||
@@ -102,23 +102,28 @@ appropriate labels will be applied for categorization.
|
||||
[getting started](https://docs.netbox.dev/en/stable/development/getting-started/)
|
||||
documentation for tips on setting up your development environment.
|
||||
|
||||
* Be sure to open an issue **before** starting work on a pull request, and
|
||||
discuss your idea with the NetBox maintainers before beginning work. This will
|
||||
help prevent wasting time on something that might we might not be able to
|
||||
implement. When suggesting a new feature, also make sure it won't conflict with
|
||||
any work that's already in progress.
|
||||
* Be sure to open an issue and wait for it to be assigned to you **before**
|
||||
starting work on a pull request, and discuss your idea with the NetBox
|
||||
maintainers before beginning work. This will help prevent wasting time on
|
||||
proposed changes that we might not be able to accept. When suggesting a new
|
||||
feature, also make sure it won't conflict with any work that's already in
|
||||
progress.
|
||||
|
||||
* Once you've opened or identified an issue you'd like to work on, ask that it
|
||||
be assigned to you so that others are aware it's being worked on. A maintainer
|
||||
will then mark the issue as "accepted."
|
||||
be assigned to you so that others are aware it's being worked on. If it meets
|
||||
the acceptance criteria, a maintainer will then mark the issue as "accepted"
|
||||
and assign it to you. (Note that GitHub requires that a user first comment on
|
||||
an issue before it can be assigned to that user.)
|
||||
|
||||
* Any pull request which does _not_ relate to an **accepted** issue will be closed.
|
||||
* Any pull request which does not relate to an **assigned** issue will be
|
||||
closed.
|
||||
|
||||
* All new functionality must include relevant tests where applicable.
|
||||
|
||||
* When submitting a pull request, please be sure to work off of the `develop`
|
||||
branch, rather than `master`. The `develop` branch is used for ongoing
|
||||
development, while `master` is used for tagging stable releases.
|
||||
development, while `master` is used for tagging stable releases. (If you're
|
||||
developing for the next minor release, use `feature` instead.)
|
||||
|
||||
* In most cases, it is not necessary to add a changelog entry: A maintainer will
|
||||
take care of this when the PR is merged. (This helps avoid merge conflicts
|
||||
@@ -136,8 +141,10 @@ these checks):
|
||||
|
||||
Only comment on an issue if you are sharing a relevant idea or constructive
|
||||
feedback. **Do not** comment on an issue just to show your support (give the
|
||||
top post a :+1: instead) or ask for an ETA. These comments will be deleted to
|
||||
reduce noise in the discussion.
|
||||
top post a :+1: instead) or to ask for an update. Doing so generates
|
||||
unnecessary noise in the discussion, and is especially annoying for people who
|
||||
have subscribed to updates for the issue. Any comments without substance
|
||||
relevant to the discussion will be deleted.
|
||||
|
||||
## Issue Lifecycle
|
||||
|
||||
|
||||
35
README.md
@@ -2,14 +2,24 @@
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||
</div>
|
||||
|
||||
NetBox is the leading solution for modeling and documenting modern networks. By
|
||||
combining the traditional disciplines of IP address management (IPAM) and
|
||||
datacenter infrastructure management (DCIM) with powerful APIs and extensions,
|
||||
NetBox provides the ideal "source of truth" to power network automation.
|
||||
Available as open source software under the Apache 2.0 license, NetBox is
|
||||
employed by thousands of organizations around the world.
|
||||
|
||||

|
||||
|
||||
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
|
||||
network automation, used by thousands of organizations around the world.
|
||||
Initially conceived by the network engineering team at
|
||||
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
|
||||
to address the needs of network and infrastructure engineers. It is intended to
|
||||
function as a domain-specific source of truth for network operations.
|
||||
[](https://github.com/netbox-community/netbox/commits)
|
||||
[](https://github.com/netbox-community/netbox/issues)
|
||||
[](https://github.com/netbox-community/netbox/pulls)
|
||||
[](https://github.com/netbox-community/netbox/graphs/contributors)
|
||||
<br />Stats via [Repography](https://repography.com)
|
||||
|
||||
## About NetBox
|
||||
|
||||

|
||||
|
||||
Myriad infrastructure components can be modeled in NetBox, including:
|
||||
|
||||
@@ -21,6 +31,7 @@ Myriad infrastructure components can be modeled in NetBox, including:
|
||||
* Virtual machines and clusters
|
||||
* IP prefixes, ranges, and addresses
|
||||
* VRFs and route targets
|
||||
* L2VPN and overlays
|
||||
* FHRP groups (VRRP, HSRP, etc.)
|
||||
* AS numbers
|
||||
* VLANs and scoped VLAN groups
|
||||
@@ -45,14 +56,16 @@ customized and extended through the use of:
|
||||
NetBox also features a complete REST API as well as a GraphQL API for easily
|
||||
integrating with other tools and systems.
|
||||
|
||||
The complete documentation for NetBox can be found at [docs.netbox.dev](https://docs.netbox.dev/).
|
||||
A public demo instance is available at [demo.netbox.dev](https://demo.netbox.dev).
|
||||
|
||||
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
|
||||
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a
|
||||
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
|
||||
|
||||
The complete documentation for NetBox can be found at [docs.netbox.dev](https://docs.netbox.dev/). A public demo instance is available at https://demo.netbox.dev.
|
||||
complete list of requirements, see `requirements.txt`. The code is available
|
||||
[on GitHub](https://github.com/netbox-community/netbox).
|
||||
|
||||
<div align="center">
|
||||
<h4>Thank you to our sponsors!</h4>
|
||||
<h3>Thank you to our sponsors!</h3>
|
||||
|
||||
[](https://try.digitalocean.com/developer-cloud)
|
||||
|
||||
@@ -90,8 +103,6 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
|
||||
|
||||
### Screenshots
|
||||
|
||||
")
|
||||
|
||||
")
|
||||
|
||||

|
||||
|
||||
@@ -68,7 +68,7 @@ drf-yasg[validation]
|
||||
|
||||
# Django wrapper for Graphene (GraphQL support)
|
||||
# https://github.com/graphql-python/graphene-django
|
||||
graphene_django
|
||||
graphene_django<3.0
|
||||
|
||||
# WSGI HTTP server
|
||||
# https://gunicorn.org/
|
||||
@@ -80,7 +80,8 @@ Jinja2
|
||||
|
||||
# Simple markup language for rendering HTML
|
||||
# https://github.com/Python-Markdown/markdown
|
||||
Markdown
|
||||
# mkdocs currently requires Markdown v3.3
|
||||
Markdown<3.4
|
||||
|
||||
# File inclusion plugin for Python-Markdown
|
||||
# https://github.com/cmacmackin/markdown-include
|
||||
|
||||
4
docs/_theme/main.html
vendored
@@ -2,8 +2,8 @@
|
||||
|
||||
{% block site_meta %}
|
||||
{{ super() }}
|
||||
{# Disable search indexing unless we're building for ReadTheDocs #}
|
||||
{% if not config.extra.readthedocs %}
|
||||
{# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
|
||||
{% if page.canonical_url != 'https://docs.netbox.dev/' %}
|
||||
<meta name="robots" content="noindex">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -58,7 +58,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
|
||||
|
||||
Default: None
|
||||
|
||||
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://2.python-requests.org/en/master/user/advanced/). For example:
|
||||
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#proxies). For example:
|
||||
|
||||
```python
|
||||
HTTP_PROXIES = {
|
||||
|
||||
@@ -129,6 +129,19 @@ The Script object provides a set of convenient functions for recording messages
|
||||
|
||||
Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages.
|
||||
|
||||
## Change Logging
|
||||
|
||||
To generate the correct change log data when editing an existing object, a snapshot of the object must be taken before making any changes to the object.
|
||||
|
||||
```python
|
||||
if obj.pk and hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
|
||||
obj.property = "New Value"
|
||||
obj.full_clean()
|
||||
obj.save()
|
||||
```
|
||||
|
||||
## Variable Reference
|
||||
|
||||
### Default Options
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
Getting started with NetBox development is pretty straightforward, and should feel very familiar to anyone with Django development experience. There are a few things you'll need:
|
||||
|
||||
* A Linux system or environment
|
||||
* A Linux system or compatible environment
|
||||
* A PostgreSQL server, which can be installed locally [per the documentation](../installation/1-postgresql.md)
|
||||
* A Redis server, which can also be [installed locally](../installation/2-redis.md)
|
||||
* A supported version of Python
|
||||
* Python 3.8 or later
|
||||
|
||||
### Fork the Repo
|
||||
### 1. Fork the Repo
|
||||
|
||||
Assuming you'll be working on your own fork, your first step will be to fork the [official git repository](https://github.com/netbox-community/netbox). (If you're a maintainer who's going to be working directly with the official repo, skip this step.) Click the "fork" button at top right (be sure that you've logged into GitHub first).
|
||||
|
||||
@@ -21,7 +21,7 @@ Copy the URL provided in the dialog box.
|
||||
|
||||
You can then clone your GitHub fork locally for development:
|
||||
|
||||
```no-highlight
|
||||
```no-highlight hl_lines="1 9"
|
||||
$ git clone https://github.com/$username/netbox.git
|
||||
Cloning into 'netbox'...
|
||||
remote: Enumerating objects: 85949, done.
|
||||
@@ -35,93 +35,114 @@ base_requirements.txt contrib docs mkdocs.yml NOTICE requ
|
||||
CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scripts
|
||||
```
|
||||
|
||||
### 2. Create a New Branch
|
||||
|
||||
The NetBox project utilizes three persistent git branches to track work:
|
||||
|
||||
* `master` - Serves as a snapshot of the current stable release
|
||||
* `develop` - All development on the upcoming stable release occurs here
|
||||
* `feature` - Tracks work on an upcoming major release
|
||||
* `develop` - All development on the upcoming stable (patch) release occurs here
|
||||
* `feature` - Tracks work on an upcoming minor release
|
||||
|
||||
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. **Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
|
||||
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. For example, assume that the current NetBox release is v3.3.5. Work applied to the `develop` branch will appear in v3.3.6, and work done under the `feature` branch will be included in the next minor release (v3.4.0).
|
||||
|
||||
For example, assume that the current NetBox release is v3.1.1. Work applied to the `develop` branch will appear in v3.1.2, and work done under the `feature` branch will be included in the next minor release (v3.2.0).
|
||||
!!! warning
|
||||
**Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
|
||||
|
||||
### Enable Pre-Commit Hooks
|
||||
To create a new branch, first ensure that you've checked out the desired base branch, then run:
|
||||
|
||||
```no-highlight
|
||||
git checkout -B $branchname
|
||||
```
|
||||
|
||||
When naming a new git branch, contributors are strongly encouraged to use the relevant issue number followed by a very brief description of the work:
|
||||
|
||||
```no-highlight
|
||||
$issue-$description
|
||||
```
|
||||
|
||||
The description should be just two or three words to imply the focus of the work being performed. For example, bug #1234 to fix a TypeError exception when creating a device might be named `1234-device-typerror`. This ensures that branches are always follow some logical ordering (e.g. when running `git branch -a`) and helps other developers quickly identify the purpose of each.
|
||||
|
||||
### 3. Enable Pre-Commit Hooks
|
||||
|
||||
NetBox ships with a [git pre-commit hook](https://githooks.com/) script that automatically checks for style compliance and missing database migrations prior to committing changes. This helps avoid erroneous commits that result in CI test failures. You are encouraged to enable it by creating a link to `scripts/git-hooks/pre-commit`:
|
||||
|
||||
```no-highlight
|
||||
$ cd .git/hooks/
|
||||
$ ln -s ../../scripts/git-hooks/pre-commit
|
||||
cd .git/hooks/
|
||||
ln -s ../../scripts/git-hooks/pre-commit
|
||||
```
|
||||
For the pre-commit hooks to work, you will also need to install the pycodestyle package:
|
||||
|
||||
### Create a Python Virtual Environment
|
||||
```no-highlight
|
||||
python -m pip install pycodestyle
|
||||
```
|
||||
...and set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md)
|
||||
|
||||
### 4. Create a Python Virtual Environment
|
||||
|
||||
A [virtual environment](https://docs.python.org/3/tutorial/venv.html) (or "venv" for short) is like a container for a set of Python packages. These allow you to build environments suited to specific projects without interfering with system packages or other projects. When installed per the documentation, NetBox uses a virtual environment in production.
|
||||
|
||||
Create a virtual environment using the `venv` Python module:
|
||||
|
||||
```no-highlight
|
||||
$ mkdir ~/.venv
|
||||
$ python3 -m venv ~/.venv/netbox
|
||||
mkdir ~/.venv
|
||||
python3 -m venv ~/.venv/netbox
|
||||
```
|
||||
|
||||
This will create a directory named `.venv/netbox/` in your home directory, which houses a virtual copy of the Python executable and its related libraries and tooling. When running NetBox for development, it will be run using the Python binary at `~/.venv/netbox/bin/python`.
|
||||
|
||||
!!! info "Where to Create Your Virtual Environments"
|
||||
Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created almost wherever you please. Also consider using [`virtualenvwrapper`](https://virtualenvwrapper.readthedocs.io/en/stable/) to simplify the management of multiple venvs.
|
||||
!!! tip "Virtual Environments"
|
||||
Keeping virtual environments in `~/.venv/` is a common convention but entirely optional: Virtual environments can be created almost wherever you please. Also consider using [`virtualenvwrapper`](https://virtualenvwrapper.readthedocs.io/en/stable/) to simplify the management of multiple environments.
|
||||
|
||||
Once created, activate the virtual environment:
|
||||
|
||||
```no-highlight
|
||||
$ source ~/.venv/netbox/bin/activate
|
||||
(netbox) $
|
||||
source ~/.venv/netbox/bin/activate
|
||||
```
|
||||
|
||||
Notice that the console prompt changes to indicate the active environment. This updates the necessary system environment variables to ensure that any Python scripts are run within the virtual environment.
|
||||
|
||||
### Install Dependencies
|
||||
### 5. Install Required Packages
|
||||
|
||||
With the virtual environment activated, install the project's required Python packages using the `pip` module:
|
||||
With the virtual environment activated, install the project's required Python packages using the `pip` module. Required packages are defined in `requirements.txt`. Each line in this file specifies the name and specific version of a required package.
|
||||
|
||||
```no-highlight
|
||||
(netbox) $ python -m pip install -r requirements.txt
|
||||
Collecting Django==3.1 (from -r requirements.txt (line 1))
|
||||
Cache entry deserialization failed, entry ignored
|
||||
Using cached https://files.pythonhosted.org/packages/2b/5a/4bd5624546912082a1bd2709d0edc0685f5c7827a278d806a20cf6adea28/Django-3.1-py3-none-any.whl
|
||||
...
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Configure NetBox
|
||||
### 6. Configure NetBox
|
||||
|
||||
Within the `netbox/netbox/` directory, copy `configuration_example.py` to `configuration.py` and update the following parameters:
|
||||
|
||||
* `ALLOWED_HOSTS`: This can be set to `['*']` for development purposes
|
||||
* `DATABASE`: PostgreSQL database connection parameters
|
||||
* `REDIS`: Redis configuration, if different from the defaults
|
||||
* `REDIS`: Redis configuration (if different from the defaults)
|
||||
* `SECRET_KEY`: Set to a random string (use `generate_secret_key.py` in the parent directory to generate a suitable key)
|
||||
* `DEBUG`: Set to `True`
|
||||
* `DEVELOPER`: Set to `True` (this enables the creation of new database migrations)
|
||||
|
||||
### Start the Development Server
|
||||
### 7. Start the Development Server
|
||||
|
||||
Django provides a lightweight, auto-updating HTTP/WSGI server for development use. It is started with the `runserver` management command:
|
||||
Django provides a lightweight, auto-updating [HTTP/WSGI server](https://docs.djangoproject.com/en/stable/ref/django-admin/#runserver) for development use. It is started with the `runserver` management command:
|
||||
|
||||
```no-highlight
|
||||
```no-highlight hl_lines="1"
|
||||
$ ./manage.py runserver
|
||||
Watching for file changes with StatReloader
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
February 18, 2022 - 20:29:57
|
||||
Django version 4.0.2, using settings 'netbox.settings'
|
||||
August 18, 2022 - 15:17:52
|
||||
Django version 4.0.7, using settings 'netbox.settings'
|
||||
Starting development server at http://127.0.0.1:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
```
|
||||
|
||||
This ensures that your development environment is now complete and operational. Any changes you make to the code base will be automatically adapted by the development server.
|
||||
This ensures that your development environment is now complete and operational. The development server will monitor the development environment and automatically reload in response to any changes made.
|
||||
|
||||
!!! info "IDE Integration"
|
||||
Some IDEs, such as PyCharm, will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
|
||||
!!! tip "IDE Integration"
|
||||
Some IDEs, such as the highly-recommended [PyCharm](https://www.jetbrains.com/pycharm/), will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment.
|
||||
|
||||
## UI Development
|
||||
|
||||
For UI development you will need to review the [Web UI Development Guide](web-ui.md)
|
||||
|
||||
## Populating Demo Data
|
||||
|
||||
@@ -131,48 +152,51 @@ The demo data is provided in JSON format and loaded into an empty database using
|
||||
|
||||
## Running Tests
|
||||
|
||||
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command, which employs Python's [`unittest`](https://docs.python.org/3/library/unittest.html#module-unittest) library. Remember to ensure the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `netbox/` directory, not the root directory of the repository.
|
||||
Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch potential errors. Tests are run using the `test` management command, which employs Python's [`unittest`](https://docs.python.org/3/library/unittest.html#module-unittest) library. Remember to ensure that the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `netbox/` directory, not the root directory of the repository.
|
||||
|
||||
To avoid potential issues with your local configuration file, set the `NETBOX_CONFIGURATION` to point to the packaged test configuration at `netbox/configuration_testing.py`. This will handle things like ensuring that the dummy plugin is enabled for comprehensive testing.
|
||||
|
||||
```no-highlight
|
||||
$ export NETBOX_CONFIGURATION=netbox.configuration_testing
|
||||
$ cd netbox/
|
||||
$ python manage.py test
|
||||
export NETBOX_CONFIGURATION=netbox.configuration_testing
|
||||
cd netbox/
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
In cases where you haven't made any changes to the database schema (which is typical), you can append the `--keepdb` argument to this command to reuse the test database between runs. This cuts down on the time it takes to run the test suite since the database doesn't have to be rebuilt each time. (Note that this argument will cause errors if you've modified any model fields since the previous test run.)
|
||||
|
||||
```no-highlight
|
||||
$ python manage.py test --keepdb
|
||||
python manage.py test --keepdb
|
||||
```
|
||||
|
||||
You can also reduce testing time by enabling parallel test execution with the `--parallel` flag. (By default, this will run as many parallel tests as you have processors. To avoid sluggishness, it's a good idea to specify a lower number of parallel tests.) This flag can be combined with `--keepdb`, although if you encounter any strange errors, try running the test suite again with parallelization disabled.
|
||||
|
||||
```no-highlight
|
||||
$ python manage.py test --parallel <n>
|
||||
python manage.py test --parallel <n>
|
||||
```
|
||||
|
||||
Finally, it's possible to limit the run to a specific set of tests, specified by their Python path. For example, to run only IPAM and DCIM view tests:
|
||||
|
||||
```no-highlight
|
||||
$ python manage.py test dcim.tests.test_views ipam.tests.test_views
|
||||
python manage.py test dcim.tests.test_views ipam.tests.test_views
|
||||
```
|
||||
|
||||
This is handy for instances where just a few tests are failing and you want to re-run them individually.
|
||||
|
||||
!!! info
|
||||
NetBox uses [django-rich](https://github.com/adamchainz/django-rich) to enhance Django's default `test` management command.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. When working on a specific issue, be sure to prefix your commit message with the word "Fixes" or "Closes" and the issue number (with a hash mark). This tells GitHub to automatically close the referenced issue once the commit has been merged.
|
||||
Once you're happy with your work and have verified that all tests pass, commit your changes and push it upstream to your fork. Always provide descriptive (but not excessively verbose) commit messages. Be sure to prefix your commit message with the word "Fixes" or "Closes" and the relevant issue number (with a hash mark). This tells GitHub to automatically close the referenced issue once the commit has been merged.
|
||||
|
||||
```no-highlight
|
||||
$ git commit -m "Closes #1234: Add IPv5 support"
|
||||
$ git push origin
|
||||
git commit -m "Closes #1234: Add IPv5 support"
|
||||
git push origin
|
||||
```
|
||||
|
||||
Once your fork has the new commit, submit a [pull request](https://github.com/netbox-community/netbox/compare) to the NetBox repo to propose the changes. Be sure to provide a detailed accounting of the changes being made and the reasons for doing so.
|
||||
|
||||
Once submitted, a maintainer will review your pull request and either merge it or request changes. If changes are needed, you can make them via new commits to your fork: The pull request will update automatically.
|
||||
|
||||
!!! note "Remember to Open an Issue First"
|
||||
!!! warning
|
||||
Remember, pull requests are permitted only for **accepted** issues. If an issue you want to work on hasn't been approved by a maintainer yet, it's best to avoid risking your time and effort on a change that might not be accepted. (The one exception to this is trivial changes to the documentation or other non-critical resources.)
|
||||
|
||||
388
docs/development/git-cheat-sheet.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# git Cheat Sheet
|
||||
|
||||
This cheat sheet serves as a convenient reference for NetBox contributors who already somewhat familiar with using git. For a general introduction to the tooling and workflows involved, please see GitHub's guide [Getting started with git](https://docs.github.com/en/get-started/getting-started-with-git/setting-your-username-in-git).
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Clone a Repo
|
||||
|
||||
This copies a remote git repository (e.g. from GitHub) to your local workstation. It will create a new directory bearing the repo's name in the current path.
|
||||
|
||||
``` title="Command"
|
||||
git clone https://github.com/$org-name/$repo-name
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git clone https://github.com/netbox-community/netbox
|
||||
Cloning into 'netbox'...
|
||||
remote: Enumerating objects: 95112, done.
|
||||
remote: Counting objects: 100% (682/682), done.
|
||||
remote: Compressing objects: 100% (246/246), done.
|
||||
remote: Total 95112 (delta 448), reused 637 (delta 436), pack-reused 94430
|
||||
Receiving objects: 100% (95112/95112), 60.40 MiB | 45.82 MiB/s, done.
|
||||
Resolving deltas: 100% (74979/74979), done.
|
||||
```
|
||||
|
||||
### Pull New Commits
|
||||
|
||||
To update your local branch with any recent upstream commits, run `git pull`.
|
||||
|
||||
``` title="Command"
|
||||
git pull
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git pull
|
||||
remote: Enumerating objects: 1, done.
|
||||
remote: Counting objects: 100% (1/1), done.
|
||||
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
Unpacking objects: 100% (1/1), done.
|
||||
From https://github.com/netbox-community/netbox
|
||||
28bc76695..e0741cc9a develop -> origin/develop
|
||||
Updating 28bc76695..e0741cc9a
|
||||
Fast-forward
|
||||
docs/release-notes/version-3.3.md | 1 +
|
||||
netbox/netbox/settings.py | 1 +
|
||||
2 files changed, 2 insertions(+)
|
||||
```
|
||||
|
||||
### List Branches
|
||||
|
||||
`git branch` lists all local branches. Appending `-a` to this command will list both local (green) and remote (red) branches.
|
||||
|
||||
``` title="Command"
|
||||
git branch -a
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git branch -a
|
||||
* develop
|
||||
remotes/origin/10170-changelog
|
||||
remotes/origin/HEAD -> origin/develop
|
||||
remotes/origin/develop
|
||||
remotes/origin/feature
|
||||
remotes/origin/master
|
||||
```
|
||||
|
||||
### Switch Branches
|
||||
|
||||
To switch to a different branch, use the `checkout` command.
|
||||
|
||||
``` title="Command"
|
||||
git checkout $branchname
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git checkout feature
|
||||
Branch 'feature' set up to track remote branch 'feature' from 'origin'.
|
||||
Switched to a new branch 'feature'
|
||||
```
|
||||
|
||||
### Create a New Branch
|
||||
|
||||
Use the `-b` argument with `checkout` to create a new _local_ branch from the current branch.
|
||||
|
||||
``` title="Command"
|
||||
git checkout -b $newbranch
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git checkout -b 123-fix-foo
|
||||
Switched to a new branch '123-fix-foo'
|
||||
```
|
||||
|
||||
### Rename a Branch
|
||||
|
||||
To rename the current branch, use the `git branch` command with the `-m` argument (for "modify").
|
||||
|
||||
``` title="Command"
|
||||
git branch -m $newname
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git branch -m jstretch-testing
|
||||
$ git branch
|
||||
develop
|
||||
feature
|
||||
* jstretch-testing
|
||||
```
|
||||
|
||||
### Merge a Branch
|
||||
|
||||
To merge one branch into another, use the `git merge` command. Start by checking out the _destination_ branch, and merge the _source_ branch into it.
|
||||
|
||||
``` title="Command"
|
||||
git merge $sourcebranch
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git checkout testing
|
||||
Switched to branch 'testing'
|
||||
Your branch is up to date with 'origin/testing'.
|
||||
$ git merge branch2
|
||||
Updating 9a12b5b5f..8ee42390b
|
||||
Fast-forward
|
||||
newfile.py | 0
|
||||
1 file changed, 0 insertions(+), 0 deletions(-)
|
||||
create mode 100644 newfile.py
|
||||
```
|
||||
|
||||
!!! warning "Avoid Merging Remote Branches"
|
||||
You generally want to avoid merging branches that exist on the remote (upstream) repository, such as `develop` and `feature`: Merges into these branches should be done via a pull request on GitHub. Only merge branches when it is necessary to consolidate work you've done locally.
|
||||
|
||||
### Show Pending Changes
|
||||
|
||||
After making changes to files in the repo, `git status` will display a summary of created, modified, and deleted files.
|
||||
|
||||
``` title="Command"
|
||||
git status
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git status
|
||||
On branch 123-fix-foo
|
||||
Changes not staged for commit:
|
||||
(use "git add <file>..." to update what will be committed)
|
||||
(use "git checkout -- <file>..." to discard changes in working directory)
|
||||
|
||||
modified: README.md
|
||||
|
||||
Untracked files:
|
||||
(use "git add <file>..." to include in what will be committed)
|
||||
|
||||
foo.py
|
||||
|
||||
no changes added to commit (use "git add" and/or "git commit -a")
|
||||
```
|
||||
|
||||
### Stage Changed Files
|
||||
|
||||
Before creating a new commit, modified files must be staged. This is typically done with the `git add` command. You can specify a particular path, or just append `-A` to automatically staged _all_ changed files within the current directory. Run `git status` again to verify what files have been staged.
|
||||
|
||||
``` title="Command"
|
||||
git add -A
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git add -A
|
||||
$ git status
|
||||
On branch 123-fix-foo
|
||||
Changes to be committed:
|
||||
(use "git reset HEAD <file>..." to unstage)
|
||||
|
||||
modified: README.md
|
||||
new file: foo.py
|
||||
|
||||
```
|
||||
|
||||
### Review Staged Files
|
||||
|
||||
It's a good idea to thoroughly review all staged changes immediately prior to creating a new commit. This can be done using the `git diff` command. Appending the `--staged` argument will show staged changes; omitting it will show changes that have not yet been staged.
|
||||
|
||||
``` title="Command"
|
||||
git diff --staged
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git diff --staged
|
||||
diff --git a/README.md b/README.md
|
||||
index 93e125079..4344fb514 100644
|
||||
--- a/README.md
|
||||
+++ b/README.md
|
||||
@@ -1,3 +1,8 @@
|
||||
+
|
||||
+Added some lines here
|
||||
+and here
|
||||
+and here too
|
||||
+
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||
</div>
|
||||
diff --git a/foo.py b/foo.py
|
||||
new file mode 100644
|
||||
index 000000000..e69de29bb
|
||||
```
|
||||
|
||||
### Create a New Commit
|
||||
|
||||
The `git commit` command records your changes to the current branch. Specify a commit message with the `-m` argument. (If omitted, a file editor will be opened to provide a message.
|
||||
|
||||
``` title="Command"
|
||||
git commit -m "Fixes #123: Fixed the thing that was broken"
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git commit -m "Fixes #123: Fixed the thing that was broken"
|
||||
[123-fix-foo 9a12b5b5f] Fixes #123: Fixed the thing that was broken
|
||||
2 files changed, 5 insertions(+)
|
||||
create mode 100644 foo.py
|
||||
```
|
||||
|
||||
!!! tip "Automatically Closing Issues"
|
||||
GitHub will [automatically close](https://github.blog/2013-01-22-closing-issues-via-commit-messages/) any issues referenced in a commit message by `Fixes:` or `Closes:` when the commit is merged into the repository's default branch. Contributors are strongly encouraged to follow this convention when forming commit messages. (Use "Closes" for feature requests and "Fixes" for bugs.)
|
||||
|
||||
### Push a Commit Upstream
|
||||
|
||||
Once you've made a commit locally, it needs to be pushed upstream to the _remote_ repository (typically called "origin"). This is done with the `git push` command. If this is a new branch that doesn't yet exist on the remote repository, you'll need to set the upstream for it when pushing.
|
||||
|
||||
``` title="Command"
|
||||
git push -u origin $branchname
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git push -u origin testing
|
||||
Counting objects: 3, done.
|
||||
Delta compression using up to 16 threads.
|
||||
Compressing objects: 100% (3/3), done.
|
||||
Writing objects: 100% (3/3), 377 bytes | 377.00 KiB/s, done.
|
||||
Total 3 (delta 2), reused 0 (delta 0)
|
||||
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
|
||||
remote:
|
||||
remote: Create a pull request for 'testing' on GitHub by visiting:
|
||||
remote: https://github.com/netbox-community/netbox/pull/new/testing
|
||||
remote:
|
||||
To https://github.com/netbox-community/netbox
|
||||
* [new branch] testing -> testing
|
||||
Branch 'testing' set up to track remote branch 'testing' from 'origin'.
|
||||
```
|
||||
|
||||
!!! tip
|
||||
You can apply the following git configuration to automatically set the upstream for all new branches. This obviates the need to specify `-u origin`.
|
||||
|
||||
```
|
||||
git config --global push.default current
|
||||
```
|
||||
|
||||
## The GitHub CLI Client
|
||||
|
||||
GitHub provides a [free CLI client](https://cli.github.com/) to simplify many aspects of interacting with GitHub repositories. Note that this utility is separate from `git`, and must be [installed separately](https://github.com/cli/cli#installation).
|
||||
|
||||
This guide provides some examples of common operations, but be sure to check out the [GitHub CLI manual](https://cli.github.com/manual/) for a complete accounting of available commands.
|
||||
|
||||
### List Open Pull Requests
|
||||
|
||||
``` title="Command"
|
||||
gh pr list
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ gh pr list
|
||||
|
||||
Showing 3 of 3 open pull requests in netbox-community/netbox
|
||||
|
||||
#10223 #7503 API Bulk-Create of Devices does not check Rack-Space 7503-bulkdevice about 17 hours ago
|
||||
#9716 Closes #9599: Add cursor pagination mode lyuyangh:cursor-pagination about 1 month ago
|
||||
#9498 Adds replication and adoption for module import sleepinggenius2:issue_9361 about 2 months ago
|
||||
```
|
||||
|
||||
### Check Out a PR
|
||||
|
||||
This command will automatically check out the remote branch associated with an open pull request.
|
||||
|
||||
``` title="Command"
|
||||
gh pr checkout $number
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ gh pr checkout 10223
|
||||
Branch '7503-bulkdevice' set up to track remote branch '7503-bulkdevice' from 'origin'.
|
||||
Switched to a new branch '7503-bulkdevice'
|
||||
```
|
||||
|
||||
## Fixing Mistakes
|
||||
|
||||
### Modify the Previous Commit
|
||||
|
||||
Sometimes you'll find that you've overlooked a necessary change and need to commit again. If you haven't pushed your most recent commit and just need to make a small tweak or two, you can _amend_ your most recent commit instead of creating a new one.
|
||||
|
||||
First, stage the desired files with `git add` and verify the changes, the issue the `git commit` command with the `--amend` argument. You can also append the `--no-edit` argument if you would like to keep the previous commit message.
|
||||
|
||||
``` title="Command"
|
||||
git commit --amend --no-edit
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git add -A
|
||||
$ git diff --staged
|
||||
$ git commit --amend --no-edit
|
||||
[testing 239b16921] Added a new file
|
||||
Date: Fri Aug 26 16:30:05 2022 -0400
|
||||
2 files changed, 1 insertion(+)
|
||||
create mode 100644 newfile.py
|
||||
```
|
||||
|
||||
!!! danger "Don't Amend After Pushing"
|
||||
Never amend a commit you've already pushed upstream unless you're **certain** no one else is working on the same branch. Force-pushing will overwrite the change history, which will break any commits from other contributors. When in doubt, create a new commit instead.
|
||||
|
||||
### Undo the Last Commit
|
||||
|
||||
The `git reset` command can be used to undo the most recent commit. (`HEAD~` is equivalent to `HEAD~1` and references the commit prior to the current HEAD.) After making and staging your changes, commit using `-c ORIG_HEAD` to replace the erroneous commit.
|
||||
|
||||
``` title="Command"
|
||||
git reset HEAD~
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git add -A
|
||||
$ git commit -m "Erroneous commit"
|
||||
[testing 09ce06736] Erroneous commit
|
||||
Date: Mon Aug 29 15:20:04 2022 -0400
|
||||
1 file changed, 1 insertion(+)
|
||||
create mode 100644 BADCHANGE
|
||||
$ git reset HEAD~
|
||||
$ rm BADFILE
|
||||
$ git add -A
|
||||
$ git commit -m "Fixed commit"
|
||||
[testing c585709f3] Fixed commit
|
||||
Date: Mon Aug 29 15:22:38 2022 -0400
|
||||
1 file changed, 65 insertions(+), 20 deletions(-)
|
||||
```
|
||||
|
||||
!!! danger "Don't Reset After Pushing"
|
||||
Resetting only works until you've pushed your local changes upstream. If you've already pushed upstream, use `git revert` instead. This will create a _new_ commit that reverts the erroneous one, but ensures that the git history remains intact.
|
||||
|
||||
### Rebase from Upstream
|
||||
|
||||
If a change has been pushed to the upstream branch since you most recently pulled it, attempting to push a new local commit will fail:
|
||||
|
||||
```
|
||||
$ git push
|
||||
To https://github.com/netbox-community/netbox.git
|
||||
! [rejected] develop -> develop (fetch first)
|
||||
error: failed to push some refs to 'https://github.com/netbox-community/netbox.git'
|
||||
hint: Updates were rejected because the remote contains work that you do
|
||||
hint: not have locally. This is usually caused by another repository pushing
|
||||
hint: to the same ref. You may want to first integrate the remote changes
|
||||
hint: (e.g., 'git pull ...') before pushing again.
|
||||
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
|
||||
```
|
||||
|
||||
To resolve this, first fetch the upstream branch to update your local copy, and then [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) your local branch to include the new changes. Once the rebase has completed, you can push your local commits upstream.
|
||||
|
||||
``` title="Commands"
|
||||
git fetch
|
||||
git rebase origin/$branchname
|
||||
```
|
||||
|
||||
``` title="Example"
|
||||
$ git fetch
|
||||
remote: Enumerating objects: 1, done.
|
||||
remote: Counting objects: 100% (1/1), done.
|
||||
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
Unpacking objects: 100% (1/1), done.
|
||||
From https://github.com/netbox-community/netbox
|
||||
815b2d8a2..8c35ebbb7 develop -> origin/develop
|
||||
$ git rebase origin/develop
|
||||
First, rewinding head to replay your work on top of it...
|
||||
Applying: Further tweaks to the PR template
|
||||
Applying: Changelog for #10176, #10217
|
||||
$ git push
|
||||
Counting objects: 9, done.
|
||||
Delta compression using up to 16 threads.
|
||||
Compressing objects: 100% (9/9), done.
|
||||
Writing objects: 100% (9/9), 1.02 KiB | 1.02 MiB/s, done.
|
||||
Total 9 (delta 6), reused 0 (delta 0)
|
||||
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
|
||||
To https://github.com/netbox-community/netbox.git
|
||||
8c35ebbb7..ada745324 develop -> develop
|
||||
```
|
||||
@@ -1,22 +1,18 @@
|
||||
# NetBox Development
|
||||
|
||||
NetBox is maintained as a [GitHub project](https://github.com/netbox-community/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Each pull request must be preceded by an **approved** issue. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
|
||||
Thanks for your interest in contributing to NetBox! This introduction covers a few important things to know before you get started.
|
||||
|
||||
## Communication
|
||||
## The Code
|
||||
|
||||
There are several official forums for communication among the developers and community members:
|
||||
NetBox and many of its related projects are maintained on [GitHub](https://github.com/netbox-community/netbox). GitHub also serves as one of our primary discussion forums. While all the code and discussion is publicly accessible, you'll need register for a [free GitHub account](https://github.com/signup) to engage in participation. Most people begin by [forking](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the NetBox repository under their own GitHub account to begin working on the code.
|
||||
|
||||
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in a GitHub issue.
|
||||
* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
||||
* [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
||||

|
||||
|
||||
## Governance
|
||||
There are three permanent branches in the repository:
|
||||
|
||||
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions.
|
||||
|
||||
## Project Structure
|
||||
|
||||
All development of the current NetBox release occurs in the `develop` branch; releases are packaged from the `master` branch. The `master` branch should _always_ represent the current stable release in its entirety, such that installing NetBox by either downloading a packaged release or cloning the `master` branch provides the same code base. Only pull requests representing new releases should be merged into `master`.
|
||||
* `master` - The current stable release. Individual changes should never be pushed directly to this branch, but rather merged from `develop`.
|
||||
* `develop` - Active development for the upcoming patch release. Pull requests will typically be based on this branch unless they introduce breaking changes that must be deferred until the next minor release.
|
||||
* `feature` - New feature work to be introduced in the next minor release (e.g. from v3.3 to v3.4).
|
||||
|
||||
NetBox components are arranged into Django apps. Each app holds the models, views, and other resources relevant to a particular function:
|
||||
|
||||
@@ -31,3 +27,34 @@ NetBox components are arranged into Django apps. Each app holds the models, view
|
||||
* `wireless`: Wireless links and LANs
|
||||
|
||||
All core functionality is stored within the `netbox/` subdirectory. HTML templates are stored in a common `templates/` directory, with model- and view-specific templates arranged by app. Documentation is kept in the `docs/` root directory.
|
||||
|
||||
## Proposing Changes
|
||||
|
||||
All substantial changes made to the code base are tracked using [GitHub issues](https://docs.github.com/en/issues). Feature requests, bug reports, and similar proposals must all be filed as issues and approved by a maintainer before work begins. This ensures that all changes to the code base are properly documented for future reference.
|
||||
|
||||
To submit a new feature request or bug report for NetBox, select and complete the appropriate [issue template](https://github.com/netbox-community/netbox/issues/new/choose). Once your issue has been approved, you're welcome to submit a [pull request](https://docs.github.com/en/pull-requests) containing your proposed changes.
|
||||
|
||||

|
||||
|
||||
Check out our [issue intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Policy) for an overview of the issue triage and approval processes.
|
||||
|
||||
!!! tip
|
||||
Avoid starting work on a proposal before it has been accepted. Not all proposed changes will be accepted, and we'd hate for you to waste time working on code that might not make it into the project.
|
||||
|
||||
## Getting Help
|
||||
|
||||
There are two primary forums for getting assistance with NetBox development:
|
||||
|
||||
* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature requests prior to submitting an issue.
|
||||
* [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained indefinitely.
|
||||
|
||||
!!! note
|
||||
Don't use GitHub issues to ask for help: These are reserved for proposed code changes only.
|
||||
|
||||
## Governance
|
||||
|
||||
NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) model of governance, with [Jeremy Stretch](https://github.com/jeremystretch) ultimately responsible for all changes to the code base. While community contributions are welcomed and encouraged, the lead maintainer's primary role is to ensure the project's long-term maintainability and continued focus on its primary functions.
|
||||
|
||||
## Licensing
|
||||
|
||||
The entire NetBox project is licensed as open source under the [Apache 2.0 license](https://github.com/netbox-community/netbox/blob/master/LICENSE.txt). This is a very permissive license which allows unlimited redistribution of all code within the project. Note that all submissions to the project are subject to the same license.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Model Types
|
||||
|
||||
A NetBox model represents a discrete object type such as a device or IP address. Each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type.
|
||||
A NetBox model represents a discrete object type such as a device or IP address. Per [Django convention](https://docs.djangoproject.com/en/stable/topics/db/models/), each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type.
|
||||
|
||||
The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework can be used to reference models within the database. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`).
|
||||
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
# Release Checklist
|
||||
|
||||
## Minor Version Bumps
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
|
||||
|
||||
### Address Pinned Dependencies
|
||||
* Major release (e.g. v2.11 to v3.0)
|
||||
* Minor release (e.g. v3.2 to v3.3)
|
||||
* Patch release (e.g. v3.3.0 to v3.3.1)
|
||||
|
||||
Check `base_requirements.txt` for any dependencies pinned to a specific version, and upgrade them to their most stable release (where possible).
|
||||
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
|
||||
### Link to the Release Notes Page
|
||||
## Minor Version Releases
|
||||
|
||||
Add the release notes (`/docs/release-notes/X.Y.md`) to the table of contents within `mkdocs.yml`, and add a summary of the major changes to `index.md`.
|
||||
### Address Constrained Dependencies
|
||||
|
||||
### Manually Perform a New Install
|
||||
|
||||
Install `mkdocs` in your local environment, then start the documentation server:
|
||||
|
||||
```no-highlight
|
||||
$ pip install -r docs/requirements.txt
|
||||
$ mkdocs serve
|
||||
```
|
||||
|
||||
Follow these instructions to perform a new installation of NetBox. This process must _not_ be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||
|
||||
### Close the Release Milestone
|
||||
|
||||
Close the release milestone on GitHub after ensuring there are no remaining open issues associated with it.
|
||||
|
||||
### Merge the Release Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release.
|
||||
|
||||
---
|
||||
|
||||
## All Releases
|
||||
|
||||
### Update Requirements
|
||||
|
||||
Required Python packages are maintained in two files. `base_requirements.txt` contains a list of all the packages required by NetBox. Some of them may be pinned to a specific version of the package due to a known issue. For example:
|
||||
Sometimes it becomes necessary to constrain dependencies to a particular version, e.g. to work around a bug in a newer release or to avoid a breaking change that we have yet to accommodate. (Another common example is to limit the upstream Django release.) For example:
|
||||
|
||||
```
|
||||
# https://github.com/encode/django-rest-framework/issues/6053
|
||||
djangorestframework==3.8.1
|
||||
```
|
||||
|
||||
The other file is `requirements.txt`, which lists each of the required packages pinned to its current stable version. When NetBox is installed, the Python environment is configured to match this file. This helps ensure that a new release of a dependency doesn't break NetBox.
|
||||
These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-requirements) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
|
||||
|
||||
Every release should refresh `requirements.txt` so that it lists the most recent stable release of each package. To do this:
|
||||
### Close the Release Milestone
|
||||
|
||||
1. Create a new virtual environment.
|
||||
2. Install the latest version of all required packages `pip install -U -r base_requirements.txt`).
|
||||
3. Run all tests and check that the UI and API function as expected.
|
||||
4. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
|
||||
5. Update the package versions in `requirements.txt` as appropriate.
|
||||
Close the [release milestone](https://github.com/netbox-community/netbox/milestones) on GitHub after ensuring there are no remaining open issues associated with it.
|
||||
|
||||
In cases where upgrading a dependency to its most recent release is breaking, it should be pinned to its current minor version in `base_requirements.txt` (with an explanatory comment) and revisited for the next major NetBox release.
|
||||
### Update the Release Notes
|
||||
|
||||
### Verify CI Build Status
|
||||
Check that a link to the release notes for the new version is present in the navigation menu (defined in `mkdocs.yml`), and that a summary of all major new features has been added to `docs/index.md`.
|
||||
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
||||
### Manually Perform a New Install
|
||||
|
||||
Start the documentation server and navigate to the current version of the installation docs:
|
||||
|
||||
```no-highlight
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||
|
||||
### Merge the Release Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
|
||||
---
|
||||
|
||||
## Patch Releases
|
||||
|
||||
### Update Requirements
|
||||
|
||||
Before each release, update each of NetBox's Python dependencies to its most recent stable version. These are defined in `requirements.txt`, which is updated from `base_requirements.txt` using `pip`. To do this:
|
||||
|
||||
1. Upgrade the installed version of all required packages in your environment (`pip install -U -r base_requirements.txt`).
|
||||
2. Run all tests and check that the UI and API function as expected.
|
||||
3. Review each requirement's release notes for any breaking or otherwise noteworthy changes.
|
||||
4. Update the package versions in `requirements.txt` as appropriate.
|
||||
|
||||
In cases where upgrading a dependency to its most recent release is breaking, it should be constrained to its current minor version in `base_requirements.txt` with an explanatory comment and revisited for the next major NetBox release (see the [Address Constrained Dependencies](#address-constrained-dependencies) section above).
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
@@ -64,28 +64,35 @@ Ensure that continuous integration testing on the `develop` branch is completing
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Replace the "FUTURE" placeholder in the release notes with the current date.
|
||||
|
||||
Commit these changes to the `develop` branch.
|
||||
Commit these changes to the `develop` branch and push upstream.
|
||||
|
||||
### Verify CI Build Status
|
||||
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully. If it fails, take action to correct the failure before proceding with the release.
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
Submit a pull request title **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Copy the documented release notes into the pull request's body.
|
||||
Submit a pull request titled **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Copy the documented release notes into the pull request's body.
|
||||
|
||||
Once CI has completed on the PR, merge it.
|
||||
Once CI has completed on the PR, merge it. This effects a new release in the `master` branch.
|
||||
|
||||
### Create a New Release
|
||||
|
||||
Draft a [new release](https://github.com/netbox-community/netbox/releases/new) with the following parameters.
|
||||
Create a [new release](https://github.com/netbox-community/netbox/releases/new) on GitHub with the following parameters.
|
||||
|
||||
* **Tag:** Current version (e.g. `v2.9.9`)
|
||||
* **Tag:** Current version (e.g. `v3.3.1`)
|
||||
* **Target:** `master`
|
||||
* **Title:** Version and date (e.g. `v2.9.9 - 2020-11-09`)
|
||||
* **Title:** Version and date (e.g. `v3.3.1 - 2022-08-25`)
|
||||
* **Description:** Copy from the pull request body
|
||||
|
||||
Copy the description from the pull request to the release.
|
||||
Once created, the release will become available for users to install.
|
||||
|
||||
### Update the Development Version
|
||||
|
||||
On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v2.9.9, set:
|
||||
On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v3.3.1, set:
|
||||
|
||||
```
|
||||
VERSION = 'v2.9.10-dev'
|
||||
VERSION = 'v3.3.2-dev'
|
||||
```
|
||||
|
||||
Commit this change with the comment "PRVB" (for _post-release version bump_) and push the commit upstream.
|
||||
|
||||
@@ -1,34 +1,53 @@
|
||||
# Style Guide
|
||||
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh` for details.
|
||||
NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations.
|
||||
|
||||
## PEP 8 Exceptions
|
||||
## Code
|
||||
|
||||
* Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
|
||||
* The library being import contains only constant declarations (e.g. `constants.py`)
|
||||
* The library being imported explicitly defines `__all__`
|
||||
### General Guidance
|
||||
|
||||
* Maximum line length is 120 characters (E501)
|
||||
* This does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and submit a separate bug report so that the entire code base can be evaluated at a later point.
|
||||
|
||||
* Line breaks are permitted following binary operators (W504)
|
||||
* Prioritize readability over concision. Python is a very flexible language that typically offers several multiple options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
|
||||
|
||||
## Enforcing Code Style
|
||||
* Include a newline at the end of every file.
|
||||
|
||||
The `pycodestyle` utility (previously `pep8`) is used by the CI process to enforce code style. It is strongly recommended to include as part of your commit process. A git commit hook is provided in the source at `scripts/git-hooks/pre-commit`. Linking to this script from `.git/hooks/` will invoke `pycodestyle` prior to every commit attempt and abort if the validation fails.
|
||||
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary code is best avoided entirely.
|
||||
|
||||
```
|
||||
$ cd .git/hooks/
|
||||
$ ln -s ../../scripts/git-hooks/pre-commit
|
||||
```
|
||||
* Constants (variables which do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
|
||||
|
||||
To invoke `pycodestyle` manually, run:
|
||||
* Every model must have a [docstring](https://peps.python.org/pep-0257/). Every custom method should include an explanation of its function.
|
||||
|
||||
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
|
||||
|
||||
### PEP 8 Exceptions
|
||||
|
||||
NetBox ignores certain PEP8 assertions. These are listed below.
|
||||
|
||||
#### Wildcard Imports
|
||||
|
||||
Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
|
||||
|
||||
* The library being import contains only constant declarations (e.g. `constants.py`)
|
||||
* The library being imported explicitly defines `__all__`
|
||||
|
||||
#### Maximum Line Length (E501)
|
||||
|
||||
NetBox does not restrict lines to a maximum length of 79 characters. We use a maximum line length of 120 characters, however this is not enforced by CI. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||
|
||||
#### Line Breaks Following Binary Operators (W504)
|
||||
|
||||
Line breaks are permitted following binary operators.
|
||||
|
||||
### Enforcing Code Style
|
||||
|
||||
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#2-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
|
||||
```
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
```
|
||||
|
||||
## Introducing New Dependencies
|
||||
### Introducing New Dependencies
|
||||
|
||||
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and supply chain attacks.
|
||||
|
||||
@@ -39,24 +58,22 @@ If there's a strong case for introducing a new dependency, it must meet the foll
|
||||
* It must be actively maintained, with no longer than one year between releases.
|
||||
* It must be available via the [Python Package Index](https://pypi.org/) (PyPI).
|
||||
|
||||
When adding a new dependency, a short description of the package and the URL of its code repository must be added to `base_requirements.txt`. Additionally, a line specifying the package name pinned to the current stable release must be added to `requirements.txt`. This ensures that NetBox will install only the known-good release and simplify support efforts.
|
||||
When adding a new dependency, a short description of the package and the URL of its code repository must be added to `base_requirements.txt`. Additionally, a line specifying the package name pinned to the current stable release must be added to `requirements.txt`. This ensures that NetBox will install only the known-good release.
|
||||
|
||||
## General Guidance
|
||||
## Written Works
|
||||
|
||||
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and submit a separate bug report so that the entire code base can be evaluated at a later point.
|
||||
### General Guidance
|
||||
|
||||
* Prioritize readability over concision. Python is a very flexible language that typically offers several options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
|
||||
* Written material must always meet a reasonable professional standard, with proper grammar, spelling, and punctuation.
|
||||
|
||||
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely.
|
||||
* Use two line breaks between paragraphs.
|
||||
|
||||
* Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
|
||||
* Use only a single space between sentences.
|
||||
|
||||
* Every model should have a docstring. Every custom method should include an explanation of its function.
|
||||
* All documentation is to be written in [Markdown](../reference/markdown.md), with modest amounts of HTML permitted where needed to overcome technical limitations.
|
||||
|
||||
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
|
||||
### Branding
|
||||
|
||||
## Branding
|
||||
|
||||
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. But never "Netbox" or any other deviation.
|
||||
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. but never "Netbox" or any other deviation.
|
||||
|
||||
* There is an SVG form of the NetBox logo at [docs/netbox_logo.svg](../netbox_logo.svg). It is preferred to use this logo for all purposes as it scales to arbitrary sizes without loss of resolution. If a raster image is required, the SVG logo should be converted to a PNG image of the prescribed size.
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
NetBox is the leading solution for modeling and documenting modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. Read on to discover why thousands of organizations worldwide put NetBox at the heart of their infrastructure.
|
||||
|
||||
[](./media/screenshots/netbox-ui.png)
|
||||
|
||||
## :material-server-network: Built for Networks
|
||||
|
||||
Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
|
||||
|
||||
@@ -7,7 +7,7 @@ This section of the documentation discusses installing and configuring the NetBo
|
||||
Begin by installing all system packages required by NetBox and its dependencies.
|
||||
|
||||
!!! warning "Python 3.8 or later required"
|
||||
NetBox v3.2 requires Python 3.8, 3.9, or 3.10.
|
||||
NetBox requires Python 3.8, 3.9, or 3.10.
|
||||
|
||||
=== "Ubuntu"
|
||||
|
||||
@@ -36,7 +36,7 @@ This documentation provides two options for installing NetBox: from a downloadab
|
||||
Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox` as the NetBox root.
|
||||
|
||||
```no-highlight
|
||||
sudo wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
|
||||
sudo wget https://github.com/netbox-community/netbox/archive/refs/tags/vX.Y.Z.tar.gz
|
||||
sudo tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||
sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox
|
||||
```
|
||||
|
||||
@@ -46,7 +46,7 @@ Next, create a file in the same directory as `configuration.py` (typically `/opt
|
||||
### General Server Configuration
|
||||
|
||||
!!! info
|
||||
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
|
||||
When using Active Directory you may need to specify a port on `AUTH_LDAP_SERVER_URI` to authenticate users from all domains in the forest. Use `3269` for secure, or `3268` for non-secure access to the GC (Global Catalog).
|
||||
|
||||
```python
|
||||
import ldap
|
||||
@@ -67,6 +67,16 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
|
||||
# Note that this is a NetBox-specific setting which sets:
|
||||
# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
LDAP_IGNORE_CERT_ERRORS = True
|
||||
|
||||
# Include this setting if you want to validate the LDAP server certificates against a CA certificate directory on your server
|
||||
# Note that this is a NetBox-specific setting which sets:
|
||||
# ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, LDAP_CA_CERT_DIR)
|
||||
LDAP_CA_CERT_DIR = '/etc/ssl/certs'
|
||||
|
||||
# Include this setting if you want to validate the LDAP server certificates against your own CA.
|
||||
# Note that this is a NetBox-specific setting which sets:
|
||||
# ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, LDAP_CA_CERT_FILE)
|
||||
LDAP_CA_CERT_FILE = '/path/to/example-CA.crt'
|
||||
```
|
||||
|
||||
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
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.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/_y5JRiW_PLM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
The following sections detail how to set up a new instance of NetBox:
|
||||
|
||||
1. [PostgreSQL database](1-postgresql.md)
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
# Upgrading to a New NetBox Release
|
||||
|
||||
## Review the Release Notes
|
||||
Upgrading NetBox to a new version is pretty simple, however users are cautioned to always review the release notes and save a backup of their current deployment prior to beginning an upgrade.
|
||||
|
||||
NetBox can generally be upgraded directly to any newer release with no interim steps, with the one exception being incrementing major versions. This can be done only from the most recent _minor_ release of the major version. For example, NetBox v2.11.8 can be upgraded to version 3.3.2 following the steps below. However, a deployment of NetBox v2.10.10 or earlier must first be upgraded to any v2.11 release, and then to any v3.x release. (This is to accommodate the consolidation of database schema migrations effected by a major version change).
|
||||
|
||||
[](../media/installation/upgrade_paths.png)
|
||||
|
||||
!!! warning "Perform a Backup"
|
||||
Always be sure to save a backup of your current NetBox deployment prior to starting the upgrade process.
|
||||
|
||||
## 1. Review the Release Notes
|
||||
|
||||
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../release-notes/index.md) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect.
|
||||
|
||||
## Update Dependencies to Required Versions
|
||||
## 2. Update Dependencies to Required Versions
|
||||
|
||||
NetBox v3.0 and later require the following:
|
||||
|
||||
@@ -14,10 +23,25 @@ NetBox v3.0 and later require the following:
|
||||
| PostgreSQL | 10 |
|
||||
| Redis | 4.0 |
|
||||
|
||||
## Install the Latest Release
|
||||
## 3. Install the Latest Release
|
||||
|
||||
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.
|
||||
|
||||
!!! warning
|
||||
Use the same method as you used to install Netbox originally
|
||||
|
||||
If you are not sure how Netbox was installed originally, check with this
|
||||
command:
|
||||
|
||||
```
|
||||
ls -ld /opt/netbox /opt/netbox/.git
|
||||
```
|
||||
|
||||
If Netbox was installed from a release package, then `/opt/netbox` will be a
|
||||
symlink pointing to the current version, and `/opt/netbox/.git` will not
|
||||
exist. If it was installed from git, then `/opt/netbox` and
|
||||
`/opt/netbox/.git` will both exist as normal directories.
|
||||
|
||||
### Option A: Download a Release
|
||||
|
||||
Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`.
|
||||
@@ -72,7 +96,7 @@ sudo git pull origin master
|
||||
|
||||
sudo git checkout v2.11.11
|
||||
|
||||
## Run the Upgrade Script
|
||||
## 4. Run the Upgrade Script
|
||||
|
||||
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `napalm` or `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
|
||||
|
||||
@@ -103,7 +127,7 @@ This script performs the following actions:
|
||||
been made to your local codebase and should be investigated. Never attempt to create new migrations unless you are
|
||||
intentionally modifying the database schema.
|
||||
|
||||
## Restart the NetBox Services
|
||||
## 5. Restart the NetBox Services
|
||||
|
||||
!!! warning
|
||||
If you are upgrading from an installation that does not use a Python virtual environment (any release prior to v2.7.9), you'll need to update the systemd service files to reference the new Python and gunicorn executables before restarting the services. These are located in `/opt/netbox/venv/bin/`. See the example service files in `/opt/netbox/contrib/` for reference.
|
||||
@@ -114,7 +138,7 @@ Finally, restart the gunicorn and RQ services:
|
||||
sudo systemctl restart netbox netbox-rq
|
||||
```
|
||||
|
||||
## Verify Housekeeping Scheduling
|
||||
## 6. Verify Housekeeping Scheduling
|
||||
|
||||
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.)
|
||||
|
||||
|
||||
BIN
docs/media/development/github.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
docs/media/development/github_new_issue.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/media/installation/upgrade_paths.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 96 KiB |
BIN
docs/media/screenshots/netbox-ui.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
@@ -34,12 +34,12 @@ To utilize a filter set in a subclass of one of NetBox's generic views (such as
|
||||
```python
|
||||
# views.py
|
||||
from netbox.views.generic import ObjectListView
|
||||
from .filtersets import MyModelFitlerSet
|
||||
from .filtersets import MyModelFilterSet
|
||||
from .models import MyModel
|
||||
|
||||
class MyModelListView(ObjectListView):
|
||||
queryset = MyModel.objects.all()
|
||||
filterset = MyModelFitlerSet
|
||||
filterset = MyModelFilterSet
|
||||
```
|
||||
|
||||
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
|
||||
|
||||
@@ -144,73 +144,73 @@ class MyModelFilterForm(NetBoxModelFilterSetForm):
|
||||
In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below.
|
||||
|
||||
::: utilities.forms.ColorField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CommentField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.JSONField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.MACAddressField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.SlugField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Choice Fields
|
||||
|
||||
::: utilities.forms.ChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.MultipleChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Dynamic Object Fields
|
||||
|
||||
::: utilities.forms.DynamicModelChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.DynamicModelMultipleChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## Content Type Fields
|
||||
|
||||
::: utilities.forms.ContentTypeChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.ContentTypeMultipleChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## CSV Import Fields
|
||||
|
||||
::: utilities.forms.CSVChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVMultipleChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVModelChoiceField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVContentTypeField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: utilities.forms.CSVMultipleContentTypeField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
@@ -32,11 +32,11 @@ schema = MyQuery
|
||||
NetBox provides two object type classes for use by plugins.
|
||||
|
||||
::: netbox.graphql.types.BaseObjectType
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.graphql.types.NetBoxObjectType
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
## GraphQL Fields
|
||||
@@ -44,9 +44,9 @@ NetBox provides two object type classes for use by plugins.
|
||||
NetBox provides two field classes for use by plugins.
|
||||
|
||||
::: netbox.graphql.fields.ObjectField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.graphql.fields.ObjectListField
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
@@ -112,6 +112,14 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
||||
|
||||
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
|
||||
|
||||
!!! tip "Accessing Config Parameters"
|
||||
Plugin configuration parameters can be accessed in `settings.PLUGINS_CONFIG`, mapped by plugin name. For example:
|
||||
|
||||
```python
|
||||
from django.conf import settings
|
||||
settings.PLUGINS_CONFIG['myplugin']['verbose_name']
|
||||
```
|
||||
|
||||
## Create setup.py
|
||||
|
||||
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
|
||||
|
||||
@@ -52,38 +52,38 @@ This will automatically apply any user-specific preferences for the table. (If u
|
||||
The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`.
|
||||
|
||||
::: netbox.tables.BooleanColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ChoiceFieldColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ColorColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ColoredLabelColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ContentTypeColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.ContentTypesColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.MarkdownColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.TagColumn
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.tables.TemplateColumn
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- __init__
|
||||
|
||||
@@ -84,24 +84,24 @@ Below are the class definitions for NetBox's object views. These views handle CR
|
||||
::: netbox.views.generic.base.BaseObjectView
|
||||
|
||||
::: netbox.views.generic.ObjectView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_object
|
||||
- get_template_name
|
||||
|
||||
::: netbox.views.generic.ObjectEditView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_object
|
||||
- alter_object
|
||||
|
||||
::: netbox.views.generic.ObjectDeleteView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_object
|
||||
|
||||
::: netbox.views.generic.ObjectChildrenView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_children
|
||||
- prep_table_data
|
||||
@@ -113,22 +113,22 @@ Below are the class definitions for NetBox's multi-object views. These views han
|
||||
::: netbox.views.generic.base.BaseMultiObjectView
|
||||
|
||||
::: netbox.views.generic.ObjectListView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_table
|
||||
- export_table
|
||||
- export_template
|
||||
|
||||
::: netbox.views.generic.BulkImportView
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.views.generic.BulkEditView
|
||||
selection:
|
||||
options:
|
||||
members: false
|
||||
|
||||
::: netbox.views.generic.BulkDeleteView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_form
|
||||
|
||||
@@ -137,12 +137,12 @@ Below are the class definitions for NetBox's multi-object views. These views han
|
||||
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
|
||||
|
||||
::: netbox.views.generic.ObjectChangeLogView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_form
|
||||
|
||||
::: netbox.views.generic.ObjectJournalView
|
||||
selection:
|
||||
options:
|
||||
members:
|
||||
- get_form
|
||||
|
||||
|
||||
@@ -1,5 +1,210 @@
|
||||
# NetBox v3.3
|
||||
|
||||
## v3.3.8 (2022-11-16)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#10356](https://github.com/netbox-community/netbox/issues/10356) - Add backplane Ethernet interface types
|
||||
* [#10902](https://github.com/netbox-community/netbox/issues/10902) - Add location selector to power feed form
|
||||
* [#10904](https://github.com/netbox-community/netbox/issues/10904) - Use front/rear port colors in cable trace SVG
|
||||
* [#10914](https://github.com/netbox-community/netbox/issues/10914) - Include "add module type" button on manufacturer view
|
||||
* [#10915](https://github.com/netbox-community/netbox/issues/10915) - Add count of L2VPNs to tenant view
|
||||
* [#10919](https://github.com/netbox-community/netbox/issues/10919) - Include device location under cable view
|
||||
* [#10920](https://github.com/netbox-community/netbox/issues/10920) - Include request cookies when queuing a custom script
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#9439](https://github.com/netbox-community/netbox/issues/9439) - Ensure thread safety of change logging functions
|
||||
* [#10709](https://github.com/netbox-community/netbox/issues/10709) - Correct UI display for `azuread-v2-tenant-oauth2` SSO backend
|
||||
* [#10829](https://github.com/netbox-community/netbox/issues/10829) - Fix bulk edit/delete buttons ad top of object lists
|
||||
* [#10837](https://github.com/netbox-community/netbox/issues/10837) - Correct cookie paths when `BASE_PATH` is set
|
||||
* [#10874](https://github.com/netbox-community/netbox/issues/10874) - Remove erroneous link for contact assignment count
|
||||
* [#10881](https://github.com/netbox-community/netbox/issues/10881) - Fix dark mode coloring for data on device status page
|
||||
* [#10891](https://github.com/netbox-community/netbox/issues/10891) - Populate tag selection list for service filter form
|
||||
* [#10897](https://github.com/netbox-community/netbox/issues/10897) - Fix form widget styling on FHRP group form
|
||||
* [#10910](https://github.com/netbox-community/netbox/issues/10910) - Fix cable creation links on power port view
|
||||
|
||||
---
|
||||
|
||||
## v3.3.7 (2022-11-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#10282](https://github.com/netbox-community/netbox/issues/10282) - Enforce advisory locks when allocating available IP addresses to prevent race conditions
|
||||
* [#10770](https://github.com/netbox-community/netbox/issues/10282) - Fix social authentication for new users
|
||||
* [#10791](https://github.com/netbox-community/netbox/issues/10791) - Permit nullifying VLAN group `scope_type` via REST API
|
||||
* [#10803](https://github.com/netbox-community/netbox/issues/10803) - Fix exception when ordering contacts by number of assignments
|
||||
* [#10809](https://github.com/netbox-community/netbox/issues/10809) - Permit nullifying site `time_zone` via REST API
|
||||
|
||||
---
|
||||
|
||||
## v3.3.6 (2022-10-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#9584](https://github.com/netbox-community/netbox/issues/9584) - Enable filtering devices by device type slug
|
||||
* [#9722](https://github.com/netbox-community/netbox/issues/9722) - Add LDAP configuration parameters to specify certificates
|
||||
* [#10580](https://github.com/netbox-community/netbox/issues/10580) - Link "assigned" checkbox in IP address table to assigned interface
|
||||
* [#10639](https://github.com/netbox-community/netbox/issues/10639) - Set cookie paths according to configured `BASE_PATH`
|
||||
* [#10685](https://github.com/netbox-community/netbox/issues/10685) - Position A/Z termination cards above the fold under circuit view
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#9669](https://github.com/netbox-community/netbox/issues/9669) - Strip colons from usernames when using remote authentication
|
||||
* [#10575](https://github.com/netbox-community/netbox/issues/10575) - Include OIDC dependencies for python-social-auth
|
||||
* [#10584](https://github.com/netbox-community/netbox/issues/10584) - Fix service clone link
|
||||
* [#10610](https://github.com/netbox-community/netbox/issues/10610) - Allow assignment of VC member to LAG on non-master peer
|
||||
* [#10643](https://github.com/netbox-community/netbox/issues/10643) - Ensure consistent display of custom fields for all model forms
|
||||
* [#10646](https://github.com/netbox-community/netbox/issues/10646) - Fix filtering of power feed by power panel when connecting a cable
|
||||
* [#10655](https://github.com/netbox-community/netbox/issues/10655) - Correct display of assigned contacts in object tables
|
||||
* [#10666](https://github.com/netbox-community/netbox/issues/10666) - Re-evaluate disabled LDAP user when processing API requests
|
||||
* [#10682](https://github.com/netbox-community/netbox/issues/10682) - Correct home view links to connection lists
|
||||
* [#10712](https://github.com/netbox-community/netbox/issues/10712) - Fix ModuleNotFoundError exception when generating API schema under Python 3.9+
|
||||
* [#10716](https://github.com/netbox-community/netbox/issues/10716) - Add left/right page plugin content embeds for tag view
|
||||
* [#10719](https://github.com/netbox-community/netbox/issues/10719) - Prevent user without sufficient permission from creating an IP address via FHRP group creation
|
||||
* [#10723](https://github.com/netbox-community/netbox/issues/10723) - Distinguish between inside/outside NAT assignments for device/VM primary IPs
|
||||
* [#10745](https://github.com/netbox-community/netbox/issues/10745) - Correct display of status field in clusters list
|
||||
* [#10746](https://github.com/netbox-community/netbox/issues/10746) - Add missing status attribute to cluster view
|
||||
|
||||
---
|
||||
|
||||
## v3.3.5 (2022-10-05)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8424](https://github.com/netbox-community/netbox/issues/8424) - Include rack elevation under device view
|
||||
* [#10352](https://github.com/netbox-community/netbox/issues/10352) - Omit extraneous URL query attributes during search
|
||||
* [#10465](https://github.com/netbox-community/netbox/issues/10465) - Improve formatting of device heights and rack positions
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
|
||||
* [#10408](https://github.com/netbox-community/netbox/issues/10408) - Fix validation when attempting to add redundant contact assignments
|
||||
* [#10423](https://github.com/netbox-community/netbox/issues/10423) - Enforce object type validation when creating journal entries
|
||||
* [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned
|
||||
* [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field
|
||||
* [#10445](https://github.com/netbox-community/netbox/issues/10445) - Avoid rounding virtual machine memory values
|
||||
* [#10460](https://github.com/netbox-community/netbox/issues/10460) - Restore missing connection details for device components
|
||||
* [#10461](https://github.com/netbox-community/netbox/issues/10461) - Enable filtering by read-only custom fields in the UI
|
||||
* [#10470](https://github.com/netbox-community/netbox/issues/10470) - Omit read-only custom fields from CSV import forms
|
||||
* [#10480](https://github.com/netbox-community/netbox/issues/10480) - Cable trace SVG links should not force a new window
|
||||
* [#10491](https://github.com/netbox-community/netbox/issues/10491) - Clarify representation of blocking contact assignments during contact deletion
|
||||
* [#10513](https://github.com/netbox-community/netbox/issues/10513) - Disable the reassignment of a module to a new device
|
||||
* [#10517](https://github.com/netbox-community/netbox/issues/10517) - Automatically inherit site assignment from cluster when creating a virtual machine
|
||||
* [#10559](https://github.com/netbox-community/netbox/issues/10559) - Permit the pinning of a VM to a particular device within a cluster which has no site assignment
|
||||
* [#10562](https://github.com/netbox-community/netbox/issues/10562) - Correct URL for contacts table tags column
|
||||
|
||||
---
|
||||
|
||||
## v3.3.4 (2022-09-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#10383](https://github.com/netbox-community/netbox/issues/10383) - Fix assignment of component templates to module types via web UI
|
||||
* [#10387](https://github.com/netbox-community/netbox/issues/10387) - Fix `MultiValueDictKeyError` exception when editing a device interface
|
||||
|
||||
---
|
||||
|
||||
## v3.3.3 (2022-09-15)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#8580](https://github.com/netbox-community/netbox/issues/8580) - Add `occupied` filter for cabled objects to filter by cable or `mark_connected`
|
||||
* [#9577](https://github.com/netbox-community/netbox/issues/9577) - Add `has_front_image` and `has_rear_image` filters for device types
|
||||
* [#10268](https://github.com/netbox-community/netbox/issues/10268) - Omit trailing ".0" in device positions within UI
|
||||
* [#10359](https://github.com/netbox-community/netbox/issues/10359) - Add region and site group columns to the devices table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#9231](https://github.com/netbox-community/netbox/issues/9231) - Fix `empty` lookup expression for string filters
|
||||
* [#10247](https://github.com/netbox-community/netbox/issues/10247) - Allow changing the pre-populated device/VM when creating new components
|
||||
* [#10250](https://github.com/netbox-community/netbox/issues/10250) - Fix exception when CableTermination validation fails during bulk import of cables
|
||||
* [#10258](https://github.com/netbox-community/netbox/issues/10258) - Enable the use of reports & scripts packaged in submodules
|
||||
* [#10259](https://github.com/netbox-community/netbox/issues/10259) - Fix `NoReverseMatch` exception when listing available prefixes with "flat" column displayed
|
||||
* [#10270](https://github.com/netbox-community/netbox/issues/10270) - Fix custom field validation when creating new services
|
||||
* [#10278](https://github.com/netbox-community/netbox/issues/10278) - Fix "create & add another" for image attachments
|
||||
* [#10294](https://github.com/netbox-community/netbox/issues/10294) - Fix spurious changelog diff for interface WWN field
|
||||
* [#10304](https://github.com/netbox-community/netbox/issues/10304) - Enable cloning for custom fields & custom links
|
||||
* [#10305](https://github.com/netbox-community/netbox/issues/10305) - Fix Virtual Chassis master field cannot be null according to the API
|
||||
* [#10307](https://github.com/netbox-community/netbox/issues/10307) - Correct value for "Passive 48V (4-pair)" PoE type selection
|
||||
* [#10333](https://github.com/netbox-community/netbox/issues/10333) - Show available values for `ui_visibility` field of CustomField for CSV import
|
||||
* [#10337](https://github.com/netbox-community/netbox/issues/10337) - Display SSO links when local authentication fails
|
||||
* [#10353](https://github.com/netbox-community/netbox/issues/10353) - Table action buttons should reserve return URL parameters
|
||||
* [#10362](https://github.com/netbox-community/netbox/issues/10362) - Correct display of custom fields when editing an L2VPN termination
|
||||
|
||||
---
|
||||
|
||||
## v3.3.2 (2022-09-02)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#9477](https://github.com/netbox-community/netbox/issues/9477) - Enable clearing applied table column ordering
|
||||
* [#10034](https://github.com/netbox-community/netbox/issues/10034) - Add L2VPN column to interface and VLAN tables
|
||||
* [#10043](https://github.com/netbox-community/netbox/issues/10043) - Add support for `limit` query parameter to available VLANs API endpoint
|
||||
* [#10060](https://github.com/netbox-community/netbox/issues/10060) - Add journal entries to global search
|
||||
* [#10195](https://github.com/netbox-community/netbox/issues/10195) - Enable filtering of device components by rack
|
||||
* [#10233](https://github.com/netbox-community/netbox/issues/10233) - Enable sorting rack elevations by facility ID
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#9328](https://github.com/netbox-community/netbox/issues/9328) - Hide available IPs when non-default ordering is applied
|
||||
* [#9481](https://github.com/netbox-community/netbox/issues/9481) - Update child device location when parent location changes
|
||||
* [#9832](https://github.com/netbox-community/netbox/issues/9832) - Improve error message when validating rack reservation units
|
||||
* [#9895](https://github.com/netbox-community/netbox/issues/9895) - Various corrections to OpenAPI spec
|
||||
* [#9962](https://github.com/netbox-community/netbox/issues/9962) - SSO login should respect `next` URL query parameter
|
||||
* [#9963](https://github.com/netbox-community/netbox/issues/9963) - Fix support for custom `CSRF_COOKIE_NAME` value
|
||||
* [#10155](https://github.com/netbox-community/netbox/issues/10155) - Fix rear port display when editing front port template for module type
|
||||
* [#10156](https://github.com/netbox-community/netbox/issues/10156) - Avoid forcing SVG image links to open in a new window
|
||||
* [#10161](https://github.com/netbox-community/netbox/issues/10161) - Restore "set null" option for custom fields during bulk edit
|
||||
* [#10176](https://github.com/netbox-community/netbox/issues/10176) - Correct utilization display for empty racks
|
||||
* [#10177](https://github.com/netbox-community/netbox/issues/10177) - Correct display of custom fields when editing VM interfaces
|
||||
* [#10178](https://github.com/netbox-community/netbox/issues/10178) - Display manufacturer name alongside device type under device view
|
||||
* [#10181](https://github.com/netbox-community/netbox/issues/10181) - Restore MultiPartParser (regression from #10031)
|
||||
* [#10184](https://github.com/netbox-community/netbox/issues/10184) - Fix vertical alignment when displaying object attributes with buttons
|
||||
* [#10208](https://github.com/netbox-community/netbox/issues/10208) - Fix permissions evaluation for interface actions dropdown menu
|
||||
* [#10217](https://github.com/netbox-community/netbox/issues/10217) - Handle exception when trace splits to multiple rear ports
|
||||
* [#10220](https://github.com/netbox-community/netbox/issues/10220) - Validate IP version when assigning primary IPs to a virtual machine
|
||||
* [#10231](https://github.com/netbox-community/netbox/issues/10231) - Correct API schema definition for several serializer fields
|
||||
|
||||
---
|
||||
|
||||
## v3.3.1 (2022-08-25)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI
|
||||
* [#9935](https://github.com/netbox-community/netbox/issues/9935) - Add 802.11ay and "other" wireless interface types
|
||||
* [#10031](https://github.com/netbox-community/netbox/issues/10031) - Enforce `application/json` content type for REST API requests
|
||||
* [#10033](https://github.com/netbox-community/netbox/issues/10033) - Disable "add termination" button for point-to-point L2VPNs with two terminations
|
||||
* [#10037](https://github.com/netbox-community/netbox/issues/10037) - Add "child interface" option to actions dropdown in interfaces list
|
||||
* [#10038](https://github.com/netbox-community/netbox/issues/10038) - Add "L2VPN termination" option to actions dropdown in interfaces list
|
||||
* [#10039](https://github.com/netbox-community/netbox/issues/10039) - Add "assign FHRP group" option to actions dropdown in interfaces list
|
||||
* [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances
|
||||
* [#10066](https://github.com/netbox-community/netbox/issues/10066) - Use fixed column widths for custom field values in UI
|
||||
* [#10133](https://github.com/netbox-community/netbox/issues/10133) - Enable nullifying device location during bulk edit
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#9663](https://github.com/netbox-community/netbox/issues/9663) - Omit available IP annotations when filtering prefix child IPs list
|
||||
* [#10040](https://github.com/netbox-community/netbox/issues/10040) - Fix exception when ordering prefixes by flat representation
|
||||
* [#10053](https://github.com/netbox-community/netbox/issues/10053) - Custom fields header should not be displayed when editing circuit terminations with no custom fields
|
||||
* [#10055](https://github.com/netbox-community/netbox/issues/10055) - Fix extraneous NAT indicator by device primary IP
|
||||
* [#10057](https://github.com/netbox-community/netbox/issues/10057) - Fix AttributeError exception when global search results include rack reservations
|
||||
* [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table
|
||||
* [#10070](https://github.com/netbox-community/netbox/issues/10070) - Add unique constraint for L2VPN slug
|
||||
* [#10087](https://github.com/netbox-community/netbox/issues/10087) - Correct display of far end in console/power/interface connections tables
|
||||
* [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation
|
||||
* [#10094](https://github.com/netbox-community/netbox/issues/10094) - Fix 404 when using "create and add another" to add contact assignments
|
||||
* [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI
|
||||
* [#10109](https://github.com/netbox-community/netbox/issues/10109) - Fix available prefixes calculation for container prefixes in the global table
|
||||
* [#10111](https://github.com/netbox-community/netbox/issues/10111) - Fix ValueError exception when searching for L2VPN objects
|
||||
* [#10118](https://github.com/netbox-community/netbox/issues/10118) - Fix display of connected LLDP neighbors for devices
|
||||
* [#10134](https://github.com/netbox-community/netbox/issues/10134) - Custom fields data serializer should return a 400 response for invalid data
|
||||
* [#10135](https://github.com/netbox-community/netbox/issues/10135) - Fix SSO support for SAML2 IDPs
|
||||
* [#10147](https://github.com/netbox-community/netbox/issues/10147) - Permit the creation of 0U device types via REST API
|
||||
|
||||
---
|
||||
|
||||
## v3.3.0 (2022-08-17)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -30,7 +30,7 @@ plugins:
|
||||
- os.chdir('netbox/')
|
||||
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
||||
- django.setup()
|
||||
rendering:
|
||||
options:
|
||||
heading_level: 3
|
||||
members_order: source
|
||||
show_root_heading: true
|
||||
@@ -38,7 +38,6 @@ plugins:
|
||||
show_root_toc_entry: false
|
||||
show_source: false
|
||||
extra:
|
||||
readthedocs: !ENV READTHEDOCS
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/netbox-community/netbox
|
||||
@@ -249,6 +248,7 @@ nav:
|
||||
- User Preferences: 'development/user-preferences.md'
|
||||
- Web UI: 'development/web-ui.md'
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 3.3: 'release-notes/version-3.3.md'
|
||||
|
||||
@@ -76,6 +76,12 @@ class ProviderNetworkForm(NetBoxModelForm):
|
||||
class CircuitTypeForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Circuit Type', (
|
||||
'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
@@ -136,6 +137,10 @@ class Circuit(NetBoxModel):
|
||||
def __str__(self):
|
||||
return self.cid
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('circuits.Provider'), CircuitType]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from circuits.models import *
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
|
||||
from .columns import CommitRateColumn
|
||||
|
||||
__all__ = (
|
||||
@@ -39,7 +40,7 @@ class CircuitTypeTable(NetBoxTable):
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
||||
|
||||
|
||||
class CircuitTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
cid = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Circuit ID'
|
||||
@@ -58,9 +59,6 @@ class CircuitTable(TenancyColumnsMixin, NetBoxTable):
|
||||
)
|
||||
commit_rate = CommitRateColumn()
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from circuits.models import *
|
||||
from django_tables2.utils import Accessor
|
||||
from tenancy.tables import ContactsColumnMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
__all__ = (
|
||||
@@ -10,7 +11,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderTable(NetBoxTable):
|
||||
class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -31,9 +32,6 @@ class ProviderTable(NetBoxTable):
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:provider_list'
|
||||
)
|
||||
|
||||
@@ -344,6 +344,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 5'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 6'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 7'),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
@@ -357,6 +358,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[6], provider_network=provider_networks[0], term_side='A', mark_connected=True),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
@@ -364,7 +366,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
def test_term_side(self):
|
||||
params = {'term_side': 'A'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
|
||||
def test_port_speed(self):
|
||||
params = {'port_speed': ['1000', '2000']}
|
||||
@@ -397,11 +399,19 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def test_provider_network(self):
|
||||
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
@@ -316,6 +316,7 @@ class NestedModuleSerializer(WritableNestedSerializer):
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPort
|
||||
@@ -325,6 +326,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePort
|
||||
@@ -334,6 +336,7 @@ class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutlet
|
||||
@@ -343,6 +346,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPort
|
||||
@@ -352,6 +356,7 @@ class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Interface
|
||||
@@ -361,6 +366,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.RearPort
|
||||
@@ -370,6 +376,7 @@ class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPort
|
||||
@@ -454,6 +461,7 @@ class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||
|
||||
class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerFeed
|
||||
|
||||
@@ -130,7 +130,7 @@ class SiteSerializer(NetBoxModelSerializer):
|
||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||
group = NestedSiteGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
time_zone = TimeZoneSerializerField(required=False)
|
||||
time_zone = TimeZoneSerializerField(required=False, allow_null=True)
|
||||
asns = SerializedPKRelatedField(
|
||||
queryset=ASN.objects.all(),
|
||||
serializer=NestedASNSerializer,
|
||||
@@ -310,7 +310,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
max_digits=4,
|
||||
decimal_places=1,
|
||||
label='Position (U)',
|
||||
min_value=decimal.Decimal(0.5),
|
||||
min_value=0,
|
||||
default=1.0
|
||||
)
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||
@@ -579,7 +579,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
||||
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_component(self, obj):
|
||||
if obj.component is None:
|
||||
return None
|
||||
@@ -693,13 +693,13 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_config_context(self, obj):
|
||||
return obj.get_config_context()
|
||||
|
||||
|
||||
class DeviceNAPALMSerializer(serializers.Serializer):
|
||||
method = serializers.DictField()
|
||||
method = serializers.JSONField()
|
||||
|
||||
|
||||
#
|
||||
@@ -975,7 +975,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
'custom_fields', 'created', 'last_updated', '_depth',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_component(self, obj):
|
||||
if obj.component is None:
|
||||
return None
|
||||
@@ -1046,7 +1046,7 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination'
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_termination(self, obj):
|
||||
serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
@@ -1076,7 +1076,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
||||
|
||||
class VirtualChassisSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer(required=False)
|
||||
master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -783,6 +783,17 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
||||
TYPE_400GE_OSFP = '400gbase-x-osfp'
|
||||
|
||||
# Ethernet Backplane
|
||||
TYPE_1GE_KX = '1000base-kx'
|
||||
TYPE_10GE_KR = '10gbase-kr'
|
||||
TYPE_10GE_KX4 = '10gbase-kx4'
|
||||
TYPE_25GE_KR = '25gbase-kr'
|
||||
TYPE_40GE_KR4 = '40gbase-kr4'
|
||||
TYPE_50GE_KR = '50gbase-kr'
|
||||
TYPE_100GE_KP4 = '100gbase-kp4'
|
||||
TYPE_100GE_KR2 = '100gbase-kr2'
|
||||
TYPE_100GE_KR4 = '100gbase-kr4'
|
||||
|
||||
# Wireless
|
||||
TYPE_80211A = 'ieee802.11a'
|
||||
TYPE_80211G = 'ieee802.11g'
|
||||
@@ -790,7 +801,9 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_80211AC = 'ieee802.11ac'
|
||||
TYPE_80211AD = 'ieee802.11ad'
|
||||
TYPE_80211AX = 'ieee802.11ax'
|
||||
TYPE_80211AY = 'ieee802.11ay'
|
||||
TYPE_802151 = 'ieee802.15.1'
|
||||
TYPE_OTHER_WIRELESS = 'other-wireless'
|
||||
|
||||
# Cellular
|
||||
TYPE_GSM = 'gsm'
|
||||
@@ -909,6 +922,20 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Ethernet (backplane)',
|
||||
(
|
||||
(TYPE_1GE_KX, '1000BASE-KX (1GE)'),
|
||||
(TYPE_10GE_KR, '10GBASE-KR (10GE)'),
|
||||
(TYPE_10GE_KX4, '10GBASE-KX4 (10GE)'),
|
||||
(TYPE_25GE_KR, '25GBASE-KR (25GE)'),
|
||||
(TYPE_40GE_KR4, '40GBASE-KR4 (40GE)'),
|
||||
(TYPE_50GE_KR, '50GBASE-KR (50GE)'),
|
||||
(TYPE_100GE_KP4, '100GBASE-KP4 (100GE)'),
|
||||
(TYPE_100GE_KR2, '100GBASE-KR2 (100GE)'),
|
||||
(TYPE_100GE_KR4, '100GBASE-KR4 (100GE)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Wireless',
|
||||
(
|
||||
@@ -918,7 +945,9 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_80211AC, 'IEEE 802.11ac'),
|
||||
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||
(TYPE_80211AX, 'IEEE 802.11ax'),
|
||||
(TYPE_80211AY, 'IEEE 802.11ay'),
|
||||
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
|
||||
(TYPE_OTHER_WIRELESS, 'Other (Wireless)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
@@ -1092,7 +1121,7 @@ class InterfacePoETypeChoices(ChoiceSet):
|
||||
(PASSIVE_24V_2PAIR, 'Passive 24V (2-pair)'),
|
||||
(PASSIVE_24V_4PAIR, 'Passive 24V (4-pair)'),
|
||||
(PASSIVE_48V_2PAIR, 'Passive 48V (2-pair)'),
|
||||
(PASSIVE_48V_2PAIR, 'Passive 48V (4-pair)'),
|
||||
(PASSIVE_48V_4PAIR, 'Passive 48V (4-pair)'),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -45,6 +45,9 @@ WIRELESS_IFACE_TYPES = [
|
||||
InterfaceTypeChoices.TYPE_80211AC,
|
||||
InterfaceTypeChoices.TYPE_80211AD,
|
||||
InterfaceTypeChoices.TYPE_80211AX,
|
||||
InterfaceTypeChoices.TYPE_80211AY,
|
||||
InterfaceTypeChoices.TYPE_802151,
|
||||
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
|
||||
]
|
||||
|
||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||
|
||||
@@ -434,6 +434,14 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
has_front_image = django_filters.BooleanFilter(
|
||||
label='Has a front image',
|
||||
method='_has_front_image'
|
||||
)
|
||||
has_rear_image = django_filters.BooleanFilter(
|
||||
label='Has a rear image',
|
||||
method='_has_rear_image'
|
||||
)
|
||||
console_ports = django_filters.BooleanFilter(
|
||||
method='_console_ports',
|
||||
label='Has console ports',
|
||||
@@ -487,6 +495,18 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
def _has_front_image(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.exclude(front_image='')
|
||||
else:
|
||||
return queryset.filter(front_image='')
|
||||
|
||||
def _has_rear_image(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.exclude(rear_image='')
|
||||
else:
|
||||
return queryset.filter(rear_image='')
|
||||
|
||||
def _console_ports(self, queryset, name, value):
|
||||
return queryset.exclude(consoleporttemplates__isnull=value)
|
||||
|
||||
@@ -780,6 +800,12 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
device_type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_type__slug',
|
||||
queryset=DeviceType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Device type (slug)',
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
label='Device type (ID)',
|
||||
@@ -1084,6 +1110,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Location (slug)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__rack',
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
rack = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__rack__name',
|
||||
queryset=Rack.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Rack (name)',
|
||||
)
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
@@ -1133,6 +1170,15 @@ class CabledObjectFilterSet(django_filters.FilterSet):
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
occupied = django_filters.BooleanFilter(
|
||||
method='filter_occupied'
|
||||
)
|
||||
|
||||
def filter_occupied(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(Q(cable__isnull=False) | Q(mark_connected=True))
|
||||
else:
|
||||
return queryset.filter(cable__isnull=True, mark_connected=False)
|
||||
|
||||
|
||||
class PathEndpointFilterSet(django_filters.FilterSet):
|
||||
@@ -1317,7 +1363,7 @@ class InterfaceFilterSet(
|
||||
try:
|
||||
devices = Device.objects.filter(pk__in=id_list)
|
||||
for device in devices:
|
||||
vc_interface_ids += device.vc_interfaces().values_list('id', flat=True)
|
||||
vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
|
||||
return queryset.filter(pk__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
@@ -3,7 +3,7 @@ from django import forms
|
||||
from dcim.models import *
|
||||
from extras.forms import CustomFieldsMixin
|
||||
from extras.models import Tag
|
||||
from utilities.forms import DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
|
||||
from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
|
||||
from .object_create import ComponentCreateForm
|
||||
|
||||
__all__ = (
|
||||
@@ -24,7 +24,7 @@ __all__ = (
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
|
||||
class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
@@ -37,6 +37,7 @@ class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
replication_fields = ('name', 'label')
|
||||
|
||||
|
||||
class ConsolePortBulkCreateForm(
|
||||
@@ -44,7 +45,7 @@ class ConsolePortBulkCreateForm(
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = ConsolePort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags')
|
||||
field_order = ('name', 'label', 'type', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class ConsoleServerPortBulkCreateForm(
|
||||
@@ -52,7 +53,7 @@ class ConsoleServerPortBulkCreateForm(
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = ConsoleServerPort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags')
|
||||
field_order = ('name', 'label', 'type', 'speed', 'description', 'tags')
|
||||
|
||||
|
||||
class PowerPortBulkCreateForm(
|
||||
@@ -60,7 +61,7 @@ class PowerPortBulkCreateForm(
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = PowerPort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
|
||||
field_order = ('name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
|
||||
|
||||
|
||||
class PowerOutletBulkCreateForm(
|
||||
@@ -68,7 +69,7 @@ class PowerOutletBulkCreateForm(
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = PowerOutlet
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags')
|
||||
field_order = ('name', 'label', 'type', 'feed_leg', 'description', 'tags')
|
||||
|
||||
|
||||
class InterfaceBulkCreateForm(
|
||||
@@ -79,7 +80,7 @@ class InterfaceBulkCreateForm(
|
||||
):
|
||||
model = Interface
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
|
||||
'name', 'label', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
|
||||
'poe_type', 'mark_connected', 'description', 'tags',
|
||||
)
|
||||
|
||||
@@ -96,13 +97,13 @@ class RearPortBulkCreateForm(
|
||||
DeviceBulkAddComponentForm
|
||||
):
|
||||
model = RearPort
|
||||
field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags')
|
||||
field_order = ('name', 'label', 'type', 'positions', 'mark_connected', 'description', 'tags')
|
||||
|
||||
|
||||
class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
model = ModuleBay
|
||||
field_order = ('name_pattern', 'label_pattern', 'position_pattern', 'description', 'tags')
|
||||
|
||||
field_order = ('name', 'label', 'position_pattern', 'description', 'tags')
|
||||
replication_fields = ('name', 'label', 'position')
|
||||
position_pattern = ExpandableNameField(
|
||||
label='Position',
|
||||
required=False,
|
||||
@@ -112,7 +113,7 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
|
||||
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
|
||||
model = DeviceBay
|
||||
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
|
||||
field_order = ('name', 'label', 'description', 'tags')
|
||||
|
||||
|
||||
class InventoryItemBulkCreateForm(
|
||||
@@ -121,6 +122,6 @@ class InventoryItemBulkCreateForm(
|
||||
):
|
||||
model = InventoryItem
|
||||
field_order = (
|
||||
'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||
'description', 'tags',
|
||||
)
|
||||
|
||||
@@ -480,7 +480,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'tenant', 'platform', 'serial', 'airflow',
|
||||
'location', 'tenant', 'platform', 'serial', 'airflow',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ def get_cable_form(a_type, b_type):
|
||||
label='Power Feed',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'powerpanel_id': f'$termination_{cable_end}_powerpanel',
|
||||
'power_panel_id': f'$termination_{cable_end}_powerpanel',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -87,6 +87,15 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Location')
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
},
|
||||
label=_('Rack')
|
||||
)
|
||||
virtual_chassis_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
required=False,
|
||||
@@ -356,6 +365,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
|
||||
('Images', ('has_front_image', 'has_rear_image')),
|
||||
('Components', (
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
|
||||
@@ -377,6 +387,20 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
has_front_image = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has a front image',
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
has_rear_image = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has a rear image',
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
console_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has console ports',
|
||||
@@ -927,12 +951,37 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
class CabledFilterForm(forms.Form):
|
||||
cabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
occupied = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PathEndpointFilterForm(CabledFilterForm):
|
||||
connected = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=StaticSelect(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = ConsolePort
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Connection', ('cabled', 'connected', 'occupied')),
|
||||
)
|
||||
type = MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -945,12 +994,13 @@ class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = ConsoleServerPort
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Connection', ('cabled', 'connected', 'occupied')),
|
||||
)
|
||||
type = MultipleChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -963,12 +1013,13 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerPort
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Connection', ('cabled', 'connected', 'occupied')),
|
||||
)
|
||||
type = MultipleChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
@@ -977,12 +1028,13 @@ class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = PowerOutlet
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Connection', ('cabled', 'connected', 'occupied')),
|
||||
)
|
||||
type = MultipleChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
@@ -991,7 +1043,7 @@ class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
@@ -999,7 +1051,8 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
|
||||
('PoE', ('poe_mode', 'poe_type')),
|
||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Connection', ('cabled', 'connected', 'occupied')),
|
||||
)
|
||||
kind = MultipleChoiceField(
|
||||
choices=InterfaceKindChoices,
|
||||
@@ -1080,11 +1133,12 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type', 'color')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Cable', ('cabled', 'occupied')),
|
||||
)
|
||||
model = FrontPort
|
||||
type = MultipleChoiceField(
|
||||
@@ -1097,12 +1151,13 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
model = RearPort
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'type', 'color')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Cable', ('cabled', 'occupied')),
|
||||
)
|
||||
type = MultipleChoiceField(
|
||||
choices=PortTypeChoices,
|
||||
@@ -1119,7 +1174,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'position')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
position = forms.CharField(
|
||||
@@ -1132,7 +1187,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1142,7 +1197,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
|
||||
@@ -78,6 +78,12 @@ class RegionForm(NetBoxModelForm):
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Region', (
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = (
|
||||
@@ -92,6 +98,12 @@ class SiteGroupForm(NetBoxModelForm):
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Site Group', (
|
||||
'parent', 'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = (
|
||||
@@ -213,6 +225,12 @@ class LocationForm(TenancyForm, NetBoxModelForm):
|
||||
class RackRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Rack Role', (
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = [
|
||||
@@ -340,6 +358,12 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||
class ManufacturerForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Manufacturer', (
|
||||
'name', 'slug', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
@@ -373,6 +397,7 @@ class DeviceTypeForm(NetBoxModelForm):
|
||||
'front_image', 'rear_image', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'airflow': StaticSelect(),
|
||||
'subdevice_role': StaticSelect(),
|
||||
'front_image': ClearableFileInput(attrs={
|
||||
'accept': DEVICETYPE_IMAGE_FORMATS
|
||||
@@ -405,6 +430,12 @@ class ModuleTypeForm(NetBoxModelForm):
|
||||
class DeviceRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Device Role', (
|
||||
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
@@ -421,6 +452,13 @@ class PlatformForm(NetBoxModelForm):
|
||||
max_length=64
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Platform', (
|
||||
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
||||
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
@@ -678,6 +716,7 @@ class ModuleForm(NetBoxModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.instance.pk:
|
||||
self.fields['device'].disabled = True
|
||||
self.fields['replicate_components'].initial = False
|
||||
self.fields['replicate_components'].disabled = True
|
||||
self.fields['adopt_components'].initial = False
|
||||
@@ -838,10 +877,21 @@ class PowerFeedForm(NetBoxModelForm):
|
||||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
location = DynamicModelChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
},
|
||||
initial_params={
|
||||
'racks': '$rack'
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'location_id': '$location',
|
||||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
@@ -849,14 +899,14 @@ class PowerFeedForm(NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
('Power Panel', ('region', 'site', 'power_panel')),
|
||||
('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
|
||||
('Power Feed', ('location', 'rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
|
||||
('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply',
|
||||
'region', 'site_group', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply',
|
||||
'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
@@ -986,47 +1036,85 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
|
||||
# Device component templates
|
||||
#
|
||||
|
||||
class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all()
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Disable reassignment of DeviceType when editing an existing instance
|
||||
if self.instance.pk:
|
||||
self.fields['device_type'].disabled = True
|
||||
|
||||
|
||||
class ModularComponentTemplateForm(ComponentTemplateForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all().all(),
|
||||
required=False
|
||||
)
|
||||
module_type = DynamicModelChoiceField(
|
||||
queryset=ModuleType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Disable reassignment of ModuleType when editing an existing instance
|
||||
if self.instance.pk:
|
||||
self.fields['module_type'].disabled = True
|
||||
|
||||
|
||||
class ConsolePortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
|
||||
)
|
||||
|
||||
class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect,
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect,
|
||||
}
|
||||
|
||||
|
||||
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class PowerPortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class PowerOutletTemplateForm(ModularComponentTemplateForm):
|
||||
power_port = DynamicModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False,
|
||||
@@ -1035,43 +1123,56 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'feed_leg': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class InterfaceTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description')),
|
||||
('PoE', ('poe_mode', 'poe_type'))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'poe_mode': StaticSelect(),
|
||||
'poe_type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class FrontPortTemplateForm(ModularComponentTemplateForm):
|
||||
rear_port = DynamicModelChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'devicetype_id': '$device_type',
|
||||
'moduletype_id': '$module_type',
|
||||
}
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
||||
'description',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
@@ -1079,48 +1180,50 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class RearPortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class ModuleBayTemplateForm(ComponentTemplateForm):
|
||||
fieldsets = (
|
||||
(None, ('device_type', 'name', 'label', 'position', 'description')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleBayTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'position', 'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class DeviceBayTemplateForm(ComponentTemplateForm):
|
||||
fieldsets = (
|
||||
(None, ('device_type', 'name', 'label', 'description')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'description',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class InventoryItemTemplateForm(ComponentTemplateForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=InventoryItemTemplate.objects.all(),
|
||||
required=False,
|
||||
@@ -1147,22 +1250,39 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
widget=forms.HiddenInput
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
|
||||
'component_type', 'component_id',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemTemplate
|
||||
fields = [
|
||||
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
|
||||
'component_type', 'component_id',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortForm(NetBoxModelForm):
|
||||
class DeviceComponentForm(NetBoxModelForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Disable reassignment of Device when editing an existing instance
|
||||
if self.instance.pk:
|
||||
self.fields['device'].disabled = True
|
||||
|
||||
|
||||
class ModularDeviceComponentForm(DeviceComponentForm):
|
||||
module = DynamicModelChoiceField(
|
||||
queryset=Module.objects.all(),
|
||||
required=False,
|
||||
@@ -1171,25 +1291,31 @@ class ConsolePortForm(NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortForm(ModularDeviceComponentForm):
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'speed': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortForm(NetBoxModelForm):
|
||||
module = DynamicModelChoiceField(
|
||||
queryset=Module.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
class ConsoleServerPortForm(ModularDeviceComponentForm):
|
||||
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -1198,42 +1324,32 @@ class ConsoleServerPortForm(NetBoxModelForm):
|
||||
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'speed': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class PowerPortForm(NetBoxModelForm):
|
||||
module = DynamicModelChoiceField(
|
||||
queryset=Module.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
class PowerPortForm(ModularDeviceComponentForm):
|
||||
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
|
||||
'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
|
||||
'description',
|
||||
'tags',
|
||||
'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletForm(NetBoxModelForm):
|
||||
module = DynamicModelChoiceField(
|
||||
queryset=Module.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
class PowerOutletForm(ModularDeviceComponentForm):
|
||||
power_port = DynamicModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False,
|
||||
@@ -1242,6 +1358,13 @@ class PowerOutletForm(NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
|
||||
'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
@@ -1249,20 +1372,12 @@ class PowerOutletForm(NetBoxModelForm):
|
||||
'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'feed_leg': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
||||
module = DynamicModelChoiceField(
|
||||
queryset=Module.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@@ -1330,8 +1445,14 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
||||
label='VRF'
|
||||
)
|
||||
|
||||
wwn = forms.CharField(
|
||||
empty_value=None,
|
||||
required=False,
|
||||
label='WWN'
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Interface', ('device', 'module', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')),
|
||||
('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
|
||||
('Addressing', ('vrf', 'mac_address', 'wwn')),
|
||||
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||
@@ -1351,7 +1472,6 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'speed': SelectSpeedWidget(),
|
||||
'poe_mode': StaticSelect(),
|
||||
@@ -1370,25 +1490,8 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
||||
'rf_channel_width': "Populated by selected channel (if set)",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Restrict LAG/bridge interface assignment by device/VC
|
||||
device_id = self.data['device'] if self.is_bound else self.initial.get('device')
|
||||
device = Device.objects.filter(pk=device_id).first()
|
||||
if device and device.virtual_chassis and device.virtual_chassis.master:
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
|
||||
|
||||
class FrontPortForm(NetBoxModelForm):
|
||||
module = DynamicModelChoiceField(
|
||||
queryset=Module.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
class FrontPortForm(ModularDeviceComponentForm):
|
||||
rear_port = DynamicModelChoiceField(
|
||||
queryset=RearPort.objects.all(),
|
||||
query_params={
|
||||
@@ -1396,6 +1499,13 @@ class FrontPortForm(NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
|
||||
'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
@@ -1403,18 +1513,15 @@ class FrontPortForm(NetBoxModelForm):
|
||||
'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class RearPortForm(NetBoxModelForm):
|
||||
module = DynamicModelChoiceField(
|
||||
queryset=Module.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
class RearPortForm(ModularDeviceComponentForm):
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -1423,33 +1530,32 @@ class RearPortForm(NetBoxModelForm):
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
class ModuleBayForm(NetBoxModelForm):
|
||||
class ModuleBayForm(DeviceComponentForm):
|
||||
fieldsets = (
|
||||
(None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleBay
|
||||
fields = [
|
||||
'device', 'name', 'label', 'position', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayForm(NetBoxModelForm):
|
||||
class DeviceBayForm(DeviceComponentForm):
|
||||
fieldsets = (
|
||||
(None, ('device', 'name', 'label', 'description', 'tags',)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = [
|
||||
'device', 'name', 'label', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
@@ -1472,10 +1578,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
).exclude(pk=device_bay.device.pk)
|
||||
|
||||
|
||||
class InventoryItemForm(NetBoxModelForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
class InventoryItemForm(DeviceComponentForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=InventoryItem.objects.all(),
|
||||
required=False,
|
||||
@@ -1522,6 +1625,12 @@ class InventoryItemForm(NetBoxModelForm):
|
||||
class InventoryItemRoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Inventory Item Role', (
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = [
|
||||
|
||||
@@ -2,46 +2,56 @@ from django import forms
|
||||
|
||||
from dcim.models import *
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
|
||||
)
|
||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
|
||||
from . import models as model_forms
|
||||
|
||||
__all__ = (
|
||||
'ComponentTemplateCreateForm',
|
||||
'DeviceComponentCreateForm',
|
||||
'ComponentCreateForm',
|
||||
'ConsolePortCreateForm',
|
||||
'ConsolePortTemplateCreateForm',
|
||||
'ConsoleServerPortCreateForm',
|
||||
'ConsoleServerPortTemplateCreateForm',
|
||||
'DeviceBayCreateForm',
|
||||
'DeviceBayTemplateCreateForm',
|
||||
'FrontPortCreateForm',
|
||||
'FrontPortTemplateCreateForm',
|
||||
'InterfaceCreateForm',
|
||||
'InterfaceTemplateCreateForm',
|
||||
'InventoryItemCreateForm',
|
||||
'ModularComponentTemplateCreateForm',
|
||||
'InventoryItemTemplateCreateForm',
|
||||
'ModuleBayCreateForm',
|
||||
'ModuleBayTemplateCreateForm',
|
||||
'PowerOutletCreateForm',
|
||||
'PowerOutletTemplateCreateForm',
|
||||
'PowerPortCreateForm',
|
||||
'PowerPortTemplateCreateForm',
|
||||
'RearPortCreateForm',
|
||||
'RearPortTemplateCreateForm',
|
||||
'VirtualChassisCreateForm',
|
||||
)
|
||||
|
||||
|
||||
class ComponentCreateForm(BootstrapMixin, forms.Form):
|
||||
class ComponentCreateForm(forms.Form):
|
||||
"""
|
||||
Subclass this form when facilitating the creation of one or more device component or component templates based on
|
||||
Subclass this form when facilitating the creation of one or more component or component template objects based on
|
||||
a name pattern.
|
||||
"""
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
label_pattern = ExpandableNameField(
|
||||
label='Label',
|
||||
name = ExpandableNameField()
|
||||
label = ExpandableNameField(
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
|
||||
)
|
||||
|
||||
# Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
|
||||
# ComponentCreateView when creating objects.
|
||||
replication_fields = ('name', 'label')
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate that all patterned fields generate an equal number of values
|
||||
patterned_fields = [
|
||||
field_name for field_name in self.fields if field_name.endswith('_pattern')
|
||||
]
|
||||
pattern_count = len(self.cleaned_data['name_pattern'])
|
||||
for field_name in patterned_fields:
|
||||
# Validate that all replication fields generate an equal number of values
|
||||
pattern_count = len(self.cleaned_data[self.replication_fields[0]])
|
||||
for field_name in self.replication_fields:
|
||||
value_count = len(self.cleaned_data[field_name])
|
||||
if self.cleaned_data[field_name] and value_count != pattern_count:
|
||||
raise forms.ValidationError({
|
||||
@@ -50,56 +60,55 @@ class ComponentCreateForm(BootstrapMixin, forms.Form):
|
||||
}, code='label_pattern_mismatch')
|
||||
|
||||
|
||||
class ComponentTemplateCreateForm(ComponentCreateForm):
|
||||
"""
|
||||
Creation form for component templates that can be assigned only to a DeviceType.
|
||||
"""
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
)
|
||||
field_order = ('device_type', 'name_pattern', 'label_pattern')
|
||||
#
|
||||
# Device component templates
|
||||
#
|
||||
|
||||
class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm):
|
||||
|
||||
class Meta(model_forms.ConsolePortTemplateForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class ModularComponentTemplateCreateForm(ComponentCreateForm):
|
||||
"""
|
||||
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
|
||||
"""
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name',
|
||||
help_text="""
|
||||
Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
|
||||
are not supported. Example: <code>[ge,xe]-0/0/[0-9]</code>. {module} is accepted as a substitution for
|
||||
the module bay position.
|
||||
"""
|
||||
)
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
module_type = DynamicModelChoiceField(
|
||||
queryset=ModuleType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
field_order = ('device_type', 'module_type', 'name_pattern', 'label_pattern')
|
||||
class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm):
|
||||
|
||||
class Meta(model_forms.ConsoleServerPortTemplateForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class DeviceComponentCreateForm(ComponentCreateForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
field_order = ('device', 'name_pattern', 'label_pattern')
|
||||
class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm):
|
||||
|
||||
class Meta(model_forms.PowerPortTemplateForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm):
|
||||
|
||||
class Meta(model_forms.PowerOutletTemplateForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm):
|
||||
|
||||
class Meta(model_forms.InterfaceTemplateForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
|
||||
rear_port = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'device_type', 'name_pattern', 'label_pattern', 'rear_port_set',
|
||||
|
||||
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
|
||||
fieldsets = (
|
||||
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description')),
|
||||
)
|
||||
|
||||
class Meta(model_forms.FrontPortTemplateForm.Meta):
|
||||
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -130,12 +139,12 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
@@ -143,16 +152,94 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
||||
}
|
||||
|
||||
|
||||
class FrontPortCreateForm(DeviceComponentCreateForm):
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm):
|
||||
|
||||
class Meta(model_forms.RearPortTemplateForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm):
|
||||
|
||||
class Meta(model_forms.DeviceBayTemplateForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
|
||||
position = ExpandableNameField(
|
||||
label='Position',
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
|
||||
)
|
||||
replication_fields = ('name', 'label', 'position')
|
||||
|
||||
class Meta(model_forms.ModuleBayTemplateForm.Meta):
|
||||
exclude = ('name', 'label', 'position')
|
||||
|
||||
|
||||
class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm):
|
||||
|
||||
class Meta(model_forms.InventoryItemTemplateForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm):
|
||||
|
||||
class Meta(model_forms.ConsolePortForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm):
|
||||
|
||||
class Meta(model_forms.ConsoleServerPortForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm):
|
||||
|
||||
class Meta(model_forms.PowerPortForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
|
||||
|
||||
class Meta(model_forms.PowerOutletForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
|
||||
|
||||
class Meta(model_forms.InterfaceForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if 'module' in self.fields:
|
||||
self.fields['name'].help_text += ' The string <code>{module}</code> will be replaced with the position ' \
|
||||
'of the assigned module, if any'
|
||||
|
||||
|
||||
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||
rear_port = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.',
|
||||
)
|
||||
field_order = (
|
||||
'device', 'name_pattern', 'label_pattern', 'rear_port_set',
|
||||
|
||||
# Override fieldsets from FrontPortForm to omit rear_port_position
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta(model_forms.FrontPortForm.Meta):
|
||||
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -176,12 +263,12 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
@@ -189,28 +276,39 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
|
||||
}
|
||||
|
||||
|
||||
class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm):
|
||||
position_pattern = ExpandableNameField(
|
||||
class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm):
|
||||
|
||||
class Meta(model_forms.RearPortForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
|
||||
|
||||
class Meta(model_forms.DeviceBayForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
|
||||
position = ExpandableNameField(
|
||||
label='Position',
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
|
||||
)
|
||||
field_order = ('device_type', 'name_pattern', 'label_pattern', 'position_pattern')
|
||||
replication_fields = ('name', 'label', 'position')
|
||||
|
||||
class Meta(model_forms.ModuleBayForm.Meta):
|
||||
exclude = ('name', 'label', 'position')
|
||||
|
||||
|
||||
class ModuleBayCreateForm(DeviceComponentCreateForm):
|
||||
position_pattern = ExpandableNameField(
|
||||
label='Position',
|
||||
required=False,
|
||||
help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
|
||||
)
|
||||
field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern')
|
||||
class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm):
|
||||
|
||||
class Meta(model_forms.InventoryItemForm.Meta):
|
||||
exclude = ('name', 'label')
|
||||
|
||||
|
||||
class InventoryItemCreateForm(ComponentCreateForm):
|
||||
# Device is assigned by the model form
|
||||
field_order = ('name_pattern', 'label_pattern')
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
class VirtualChassisCreateForm(NetBoxModelForm):
|
||||
region = DynamicModelChoiceField(
|
||||
|
||||
@@ -281,15 +281,11 @@ class CableTermination(models.Model):
|
||||
|
||||
# Validate interface type (if applicable)
|
||||
if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
|
||||
raise ValidationError({
|
||||
'termination': f'Cables cannot be terminated to {self.termination.get_type_display()} interfaces'
|
||||
})
|
||||
raise ValidationError(f"Cables cannot be terminated to {self.termination.get_type_display()} interfaces")
|
||||
|
||||
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
|
||||
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
|
||||
raise ValidationError({
|
||||
'termination': "Circuit terminations attached to a provider network may not be cabled."
|
||||
})
|
||||
raise ValidationError("Circuit terminations attached to a provider network may not be cabled.")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -677,6 +673,12 @@ class CablePath(models.Model):
|
||||
"""
|
||||
Return all available next segments in a split cable path.
|
||||
"""
|
||||
rearports = self.path_objects[-1]
|
||||
nodes = self.path_objects[-1]
|
||||
|
||||
return FrontPort.objects.filter(rear_port__in=rearports)
|
||||
# RearPort splitting to multiple FrontPorts with no stack position
|
||||
if type(nodes[0]) is RearPort:
|
||||
return FrontPort.objects.filter(rear_port__in=nodes)
|
||||
# Cable terminating to multiple FrontPorts mapped to different
|
||||
# RearPorts connected to different cables
|
||||
elif type(nodes[0]) is FrontPort:
|
||||
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
|
||||
|
||||
@@ -908,18 +908,20 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device != self.device:
|
||||
raise ValidationError({
|
||||
"rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
|
||||
})
|
||||
if hasattr(self, 'rear_port'):
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError({
|
||||
"rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
|
||||
f"{self.rear_port.name} has only {self.rear_port.positions} positions"
|
||||
})
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device != self.device:
|
||||
raise ValidationError({
|
||||
"rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
|
||||
})
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError({
|
||||
"rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
|
||||
f"{self.rear_port.name} has only {self.rear_port.positions} positions"
|
||||
})
|
||||
|
||||
|
||||
class RearPort(ModularComponentModel, CabledObjectModel):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import decimal
|
||||
|
||||
import yaml
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@@ -159,9 +161,17 @@ class DeviceType(NetBoxModel):
|
||||
self._original_front_image = self.front_image
|
||||
self._original_rear_image = self.rear_image
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [Manufacturer, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicetype', args=[self.pk])
|
||||
|
||||
@property
|
||||
def get_full_name(self):
|
||||
return f"{ self.manufacturer } { self.model }"
|
||||
|
||||
def to_yaml(self):
|
||||
data = {
|
||||
'manufacturer': self.manufacturer.name,
|
||||
@@ -338,6 +348,10 @@ class ModuleType(NetBoxModel):
|
||||
def __str__(self):
|
||||
return self.model
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [Manufacturer, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:moduletype', args=[self.pk])
|
||||
|
||||
@@ -658,6 +672,10 @@ class Device(NetBoxModel, ConfigContextModel):
|
||||
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
||||
return super().__str__()
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('dcim.Site'), DeviceRole, DeviceType, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
|
||||
@@ -850,6 +868,7 @@ class Device(NetBoxModel, ConfigContextModel):
|
||||
for device in devices:
|
||||
device.site = self.site
|
||||
device.rack = self.rack
|
||||
device.location = self.location
|
||||
device.save()
|
||||
|
||||
@property
|
||||
@@ -968,6 +987,14 @@ class Module(NetBoxModel, ConfigContextModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:module', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.module_bay.device != self.device:
|
||||
raise ValidationError(
|
||||
f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = self.pk is None
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@@ -54,6 +55,10 @@ class PowerPanel(NetBoxModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('dcim.Site'), ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerpanel', args=[self.pk])
|
||||
|
||||
@@ -138,6 +143,10 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [PowerPanel, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:powerfeed', args=[self.pk])
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import decimal
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -201,6 +202,10 @@ class Rack(NetBoxModel):
|
||||
return f'{self.name} ({self.facility_id})'
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('dcim.Site'), ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
|
||||
@@ -345,7 +350,7 @@ class Rack(NetBoxModel):
|
||||
# Remove units without enough space above them to accommodate a device of the specified height
|
||||
available_units = []
|
||||
for u in units:
|
||||
if set(drange(u, u + u_height, 0.5)).issubset(units):
|
||||
if set(drange(u, u + decimal.Decimal(u_height), 0.5)).issubset(units):
|
||||
available_units.append(u)
|
||||
|
||||
return list(reversed(available_units))
|
||||
@@ -410,12 +415,13 @@ class Rack(NetBoxModel):
|
||||
"""
|
||||
# Determine unoccupied units
|
||||
total_units = len(list(self.units))
|
||||
available_units = self.get_available_units()
|
||||
available_units = self.get_available_units(u_height=0.5)
|
||||
|
||||
# Remove reserved units
|
||||
for u in self.get_reserved_units():
|
||||
if u in available_units:
|
||||
available_units.remove(u)
|
||||
for ru in self.get_reserved_units():
|
||||
for u in drange(ru, ru + 1, 0.5):
|
||||
if u in available_units:
|
||||
available_units.remove(u)
|
||||
|
||||
occupied_unit_count = total_units - len(available_units)
|
||||
percentage = float(occupied_unit_count) / total_units * 100
|
||||
@@ -477,6 +483,10 @@ class RackReservation(NetBoxModel):
|
||||
def __str__(self):
|
||||
return "Reservation for rack {}".format(self.rack)
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('dcim.Site'), Rack, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rackreservation', args=[self.pk])
|
||||
|
||||
|
||||
@@ -411,6 +411,10 @@ class Location(NestedGroupModel):
|
||||
|
||||
super().validate_unique(exclude=exclude)
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [Site, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:location', args=[self.pk])
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class Node(Hyperlink):
|
||||
"""
|
||||
|
||||
def __init__(self, position, width, url, color, labels, radius=10, **extra):
|
||||
super(Node, self).__init__(href=url, target='_blank', **extra)
|
||||
super(Node, self).__init__(href=url, target='_parent', **extra)
|
||||
|
||||
x, y = position
|
||||
|
||||
@@ -94,7 +94,7 @@ class Connector(Group):
|
||||
self.add(cable)
|
||||
|
||||
# Add link
|
||||
link = Hyperlink(href=url, target='_blank')
|
||||
link = Hyperlink(href=url, target='_parent')
|
||||
|
||||
# Add text label(s)
|
||||
cursor = start[1]
|
||||
@@ -166,7 +166,7 @@ class CableTraceSVG:
|
||||
"""
|
||||
if hasattr(instance, 'parent_object'):
|
||||
# Termination
|
||||
return 'f0f0f0'
|
||||
return getattr(instance, 'color', 'f0f0f0') or 'f0f0f0'
|
||||
if hasattr(instance, 'device_role'):
|
||||
# Device
|
||||
return instance.device_role.color
|
||||
@@ -281,7 +281,7 @@ class CableTraceSVG:
|
||||
self.cursor += PADDING * 2
|
||||
|
||||
# Add link
|
||||
link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_blank')
|
||||
link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_parent')
|
||||
|
||||
# Add text label(s)
|
||||
for i, label in enumerate(labels):
|
||||
|
||||
@@ -9,6 +9,7 @@ from svgwrite.text import Text
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db.models import Q
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
@@ -41,7 +42,7 @@ def get_device_description(device):
|
||||
device.device_role,
|
||||
device.device_type.manufacturer.name,
|
||||
device.device_type.model,
|
||||
device.device_type.u_height,
|
||||
floatformat(device.device_type.u_height),
|
||||
device.asset_tag or '',
|
||||
device.serial or ''
|
||||
)
|
||||
@@ -151,7 +152,7 @@ class RackElevationSVG:
|
||||
css_extra = ' shaded' if is_shaded else ''
|
||||
|
||||
# Create hyperlink element
|
||||
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target='_blank')
|
||||
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target="_parent")
|
||||
link.set_desc(description)
|
||||
|
||||
# Add rect element to hyperlink
|
||||
@@ -235,10 +236,7 @@ class RackElevationSVG:
|
||||
self.margin_width,
|
||||
u_height * self.unit_height
|
||||
)
|
||||
link = Hyperlink(
|
||||
href='{}{}'.format(self.base_url, reservation.get_absolute_url()),
|
||||
target='_blank'
|
||||
)
|
||||
link = Hyperlink(href=f'{self.base_url}{reservation.get_absolute_url()}', target='_parent')
|
||||
link.set_desc(f'Reservation #{reservation.pk}: {reservation.description}')
|
||||
link.add(
|
||||
Rect(coords, size, class_='reservation')
|
||||
@@ -268,7 +266,7 @@ class RackElevationSVG:
|
||||
y_offset + self.unit_height / 2
|
||||
)
|
||||
|
||||
link = Hyperlink(href=url_string.format(unit), target='_blank')
|
||||
link = Hyperlink(href=url_string.format(unit), target='_parent')
|
||||
link.add(Rect((x_offset, y_offset), (self.unit_width, self.unit_height), class_='slot'))
|
||||
link.add(Text('add device', insert=text_coords, class_='add-device'))
|
||||
|
||||
|
||||
@@ -1,109 +1,8 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from netbox.tables import BaseTable, columns
|
||||
from dcim.models import ConsolePort, Interface, PowerPort
|
||||
from .cables import *
|
||||
from .connections import *
|
||||
from .devices import *
|
||||
from .devicetypes import *
|
||||
from .modules import *
|
||||
from .power import *
|
||||
from .racks import *
|
||||
from .sites import *
|
||||
|
||||
|
||||
#
|
||||
# Device connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionTable(BaseTable):
|
||||
console_server = tables.Column(
|
||||
accessor=Accessor('_path__destination__device'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Console Server'
|
||||
)
|
||||
console_server_port = tables.Column(
|
||||
accessor=Accessor('_path__destination'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Port'
|
||||
)
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Console Port'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
|
||||
|
||||
|
||||
class PowerConnectionTable(BaseTable):
|
||||
pdu = tables.Column(
|
||||
accessor=Accessor('_path__destination__device'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='PDU'
|
||||
)
|
||||
outlet = tables.Column(
|
||||
accessor=Accessor('_path__destination'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Outlet'
|
||||
)
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Power Port'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
|
||||
|
||||
|
||||
class InterfaceConnectionTable(BaseTable):
|
||||
device_a = tables.Column(
|
||||
accessor=Accessor('device'),
|
||||
linkify=True,
|
||||
verbose_name='Device A'
|
||||
)
|
||||
interface_a = tables.Column(
|
||||
accessor=Accessor('name'),
|
||||
linkify=True,
|
||||
verbose_name='Interface A'
|
||||
)
|
||||
device_b = tables.Column(
|
||||
accessor=Accessor('_path__destination__device'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Device B'
|
||||
)
|
||||
interface_b = tables.Column(
|
||||
accessor=Accessor('_path__destination'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Interface B'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')
|
||||
|
||||
71
netbox/dcim/tables/connections.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from netbox.tables import BaseTable, columns
|
||||
from dcim.models import ConsolePort, Interface, PowerPort
|
||||
from .devices import PathEndpointTable
|
||||
|
||||
__all__ = (
|
||||
'ConsoleConnectionTable',
|
||||
'InterfaceConnectionTable',
|
||||
'PowerConnectionTable',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Device connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionTable(PathEndpointTable):
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Console Port'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'connection', 'reachable')
|
||||
|
||||
|
||||
class PowerConnectionTable(PathEndpointTable):
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Power Port'
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('device', 'name', 'connection', 'reachable')
|
||||
|
||||
|
||||
class InterfaceConnectionTable(PathEndpointTable):
|
||||
device = tables.Column(
|
||||
accessor=Accessor('device'),
|
||||
linkify=True
|
||||
)
|
||||
interface = tables.Column(
|
||||
accessor=Accessor('name'),
|
||||
linkify=True
|
||||
)
|
||||
reachable = columns.BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device', 'interface', 'connection', 'reachable')
|
||||
@@ -1,12 +1,26 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
|
||||
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
|
||||
ConsolePort,
|
||||
ConsoleServerPort,
|
||||
Device,
|
||||
DeviceBay,
|
||||
DeviceRole,
|
||||
FrontPort,
|
||||
Interface,
|
||||
InventoryItem,
|
||||
InventoryItemRole,
|
||||
ModuleBay,
|
||||
Platform,
|
||||
PowerOutlet,
|
||||
PowerPort,
|
||||
RearPort,
|
||||
VirtualChassis,
|
||||
)
|
||||
from django_tables2.utils import Accessor
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
|
||||
from .template_code import *
|
||||
|
||||
__all__ = (
|
||||
@@ -137,12 +151,21 @@ class PlatformTable(NetBoxTable):
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.TemplateColumn(
|
||||
order_by=('_name',),
|
||||
template_code=DEVICE_LINK
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
region = tables.Column(
|
||||
accessor=Accessor('site__region'),
|
||||
linkify=True
|
||||
)
|
||||
site_group = tables.Column(
|
||||
accessor=Accessor('site__group'),
|
||||
linkify=True,
|
||||
verbose_name='Site Group'
|
||||
)
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -152,6 +175,9 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
|
||||
rack = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
position = columns.TemplateColumn(
|
||||
template_code='{{ value|floatformat }}'
|
||||
)
|
||||
device_role = columns.ColoredLabelColumn(
|
||||
verbose_name='Role'
|
||||
)
|
||||
@@ -189,9 +215,6 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name='VC Priority'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:device_list'
|
||||
)
|
||||
@@ -199,10 +222,10 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Device
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
|
||||
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
|
||||
'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
|
||||
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face',
|
||||
'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
|
||||
'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
||||
@@ -483,6 +506,12 @@ class BaseInterfaceTable(NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name='FHRP Groups'
|
||||
)
|
||||
l2vpn = tables.Column(
|
||||
accessor=tables.A('l2vpn_termination__l2vpn'),
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
verbose_name='L2VPN'
|
||||
)
|
||||
untagged_vlan = tables.Column(linkify=True)
|
||||
tagged_vlans = columns.TemplateColumn(
|
||||
template_code=INTERFACE_TAGGED_VLANS,
|
||||
@@ -520,8 +549,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses',
|
||||
'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'l2vpn',
|
||||
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
@@ -554,8 +583,8 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
|
||||
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
|
||||
'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
|
||||
'tagged_vlans', 'actions',
|
||||
'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
order_by = ('name',)
|
||||
default_columns = (
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
|
||||
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||
ConsolePortTemplate,
|
||||
ConsoleServerPortTemplate,
|
||||
DeviceBayTemplate,
|
||||
DeviceType,
|
||||
FrontPortTemplate,
|
||||
InterfaceTemplate,
|
||||
InventoryItemTemplate,
|
||||
Manufacturer,
|
||||
ModuleBayTemplate,
|
||||
PowerOutletTemplate,
|
||||
PowerPortTemplate,
|
||||
RearPortTemplate,
|
||||
)
|
||||
from tenancy.tables import ContactsColumnMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||
|
||||
__all__ = (
|
||||
@@ -27,7 +39,7 @@ __all__ = (
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerTable(NetBoxTable):
|
||||
class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -43,9 +55,6 @@ class ManufacturerTable(NetBoxTable):
|
||||
verbose_name='Platforms'
|
||||
)
|
||||
slug = tables.Column()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:manufacturer_list'
|
||||
)
|
||||
@@ -85,6 +94,9 @@ class DeviceTypeTable(NetBoxTable):
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:devicetype_list'
|
||||
)
|
||||
u_height = columns.TemplateColumn(
|
||||
template_code='{{ value|floatformat }}'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = DeviceType
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import PowerFeed, PowerPanel
|
||||
from tenancy.tables import ContactsColumnMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
from .devices import CableTerminationTable
|
||||
|
||||
__all__ = (
|
||||
@@ -14,7 +16,7 @@ __all__ = (
|
||||
# Power panels
|
||||
#
|
||||
|
||||
class PowerPanelTable(NetBoxTable):
|
||||
class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -29,9 +31,6 @@ class PowerPanelTable(NetBoxTable):
|
||||
url_params={'power_panel_id': 'pk'},
|
||||
verbose_name='Feeds'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:powerpanel_list'
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Rack, RackReservation, RackRole
|
||||
from django_tables2.utils import Accessor
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
|
||||
__all__ = (
|
||||
'RackTable',
|
||||
@@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
order_by=('_name',),
|
||||
linkify=True
|
||||
@@ -51,7 +51,7 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
|
||||
status = columns.ChoiceFieldColumn()
|
||||
role = columns.ColoredLabelColumn()
|
||||
u_height = tables.TemplateColumn(
|
||||
template_code="{{ record.u_height }}U",
|
||||
template_code="{{ value }}U",
|
||||
verbose_name='Height'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
@@ -68,9 +68,6 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name='Power'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:rack_list'
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin
|
||||
|
||||
from .template_code import LOCATION_BUTTONS
|
||||
|
||||
__all__ = (
|
||||
@@ -17,7 +18,7 @@ __all__ = (
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionTable(NetBoxTable):
|
||||
class RegionTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = columns.MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
@@ -26,9 +27,6 @@ class RegionTable(NetBoxTable):
|
||||
url_params={'region_id': 'pk'},
|
||||
verbose_name='Sites'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:region_list'
|
||||
)
|
||||
@@ -46,7 +44,7 @@ class RegionTable(NetBoxTable):
|
||||
# Site groups
|
||||
#
|
||||
|
||||
class SiteGroupTable(NetBoxTable):
|
||||
class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
|
||||
name = columns.MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
@@ -55,9 +53,6 @@ class SiteGroupTable(NetBoxTable):
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name='Sites'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:sitegroup_list'
|
||||
)
|
||||
@@ -75,7 +70,7 @@ class SiteGroupTable(NetBoxTable):
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -97,9 +92,6 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name='ASN Count'
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:site_list'
|
||||
)
|
||||
@@ -118,7 +110,7 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
|
||||
# Locations
|
||||
#
|
||||
|
||||
class LocationTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
name = columns.MPTTColumn(
|
||||
linkify=True
|
||||
)
|
||||
@@ -136,9 +128,6 @@ class LocationTable(TenancyColumnsMixin, NetBoxTable):
|
||||
url_params={'location_id': 'pk'},
|
||||
verbose_name='Devices'
|
||||
)
|
||||
contacts = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:location_list'
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ LINKTERMINATION = """
|
||||
<a href="{{ termination.parent_object.get_absolute_url }}">{{ termination.parent_object }}</a>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>{% if not forloop.last %},{% endif %}
|
||||
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||
{% empty %}
|
||||
{{ ''|placeholder }}
|
||||
{% endfor %}
|
||||
@@ -33,7 +33,7 @@ DEVICEBAY_STATUS = """
|
||||
|
||||
INTERFACE_IPADDRESSES = """
|
||||
<div class="table-badge-group">
|
||||
{% for ip in record.ip_addresses.all %}
|
||||
{% for ip in value.all %}
|
||||
{% if ip.status != 'active' %}
|
||||
<a href="{{ ip.get_absolute_url }}" class="table-badge badge bg-{{ ip.get_status_color }}" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}">{{ ip }}</a>
|
||||
{% else %}
|
||||
@@ -53,7 +53,7 @@ INTERFACE_FHRPGROUPS = """
|
||||
|
||||
INTERFACE_TAGGED_VLANS = """
|
||||
{% if record.mode == 'tagged' %}
|
||||
{% for vlan in record.tagged_vlans.all %}
|
||||
{% for vlan in value.all %}
|
||||
<a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
|
||||
{% endfor %}
|
||||
{% elif record.mode == 'tagged-all' %}
|
||||
@@ -62,7 +62,7 @@ INTERFACE_TAGGED_VLANS = """
|
||||
"""
|
||||
|
||||
INTERFACE_WIRELESS_LANS = """
|
||||
{% for wlan in record.wireless_lans.all %}
|
||||
{% for wlan in value.all %}
|
||||
<a href="{{ wlan.get_absolute_url }}">{{ wlan }}</a><br />
|
||||
{% endfor %}
|
||||
"""
|
||||
@@ -226,7 +226,7 @@ POWEROUTLET_BUTTONS = """
|
||||
"""
|
||||
|
||||
INTERFACE_BUTTONS = """
|
||||
{% if perms.ipam.add_ipaddress or perms.dcim.add_inventoryitem %}
|
||||
{% if perms.dcim.change_interface %}
|
||||
<span class="dropdown">
|
||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Add">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
||||
@@ -238,6 +238,15 @@ INTERFACE_BUTTONS = """
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ record.device_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Inventory Item</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ record.device_id }}&parent={{ record.pk }}&name={{ record.name }}.&type=virtual&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Child Interface</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_l2vpntermination %}
|
||||
<li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?device={{ object.pk }}&interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_fhrpgroupassignment %}
|
||||
<li><a class="dropdown-item" href="{% url 'ipam:fhrpgroupassignment_add' %}?interface_type={{ record|content_type_id }}&interface_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Assign FHRP Group</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -461,16 +461,19 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Device Type 4',
|
||||
'slug': 'device-type-4',
|
||||
'u_height': 0,
|
||||
},
|
||||
{
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Device Type 5',
|
||||
'slug': 'device-type-5',
|
||||
'u_height': 0.5,
|
||||
},
|
||||
{
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'model': 'Device Type 6',
|
||||
'slug': 'device-type-6',
|
||||
'u_height': 1,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -2054,6 +2057,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'domain': 'newdomain',
|
||||
'master': None
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -688,7 +688,7 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
device_types = (
|
||||
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True),
|
||||
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png'),
|
||||
DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR),
|
||||
DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT),
|
||||
)
|
||||
@@ -753,9 +753,9 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_is_full_depth(self):
|
||||
params = {'is_full_depth': 'true'}
|
||||
params = {'is_full_depth': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'is_full_depth': 'false'}
|
||||
params = {'is_full_depth': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_subdevice_role(self):
|
||||
@@ -773,6 +773,18 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_has_front_image(self):
|
||||
params = {'has_front_image': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'has_front_image': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_has_rear_image(self):
|
||||
params = {'has_rear_image': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'has_rear_image': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_console_ports(self):
|
||||
params = {'console_ports': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1631,6 +1643,8 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
device_types = DeviceType.objects.all()[:2]
|
||||
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'device_type': [device_types[0].slug, device_types[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_devicerole(self):
|
||||
device_roles = DeviceRole.objects.all()[:2]
|
||||
@@ -1924,10 +1938,17 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@@ -1976,12 +1997,6 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -2003,6 +2018,20 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
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]}
|
||||
@@ -2015,17 +2044,22 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'module_id': [modules[0].pk, modules[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_location(self):
|
||||
locations = Location.objects.all()[:2]
|
||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': 'true'}
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'cabled': 'false'}
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
@@ -2071,10 +2105,17 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@@ -2123,12 +2164,6 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -2157,6 +2192,13 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
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]}
|
||||
@@ -2170,9 +2212,21 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': 'true'}
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'cabled': 'false'}
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
@@ -2218,10 +2272,17 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@@ -2278,12 +2339,6 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'allocated_draw': [50, 100]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -2312,6 +2367,13 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
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]}
|
||||
@@ -2325,9 +2387,21 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': 'true'}
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'cabled': 'false'}
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
@@ -2373,10 +2447,17 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@@ -2429,12 +2510,6 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'feed_leg': [PowerOutletFeedLegChoices.FEED_LEG_A, PowerOutletFeedLegChoices.FEED_LEG_B]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -2463,6 +2538,13 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
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]}
|
||||
@@ -2476,9 +2558,21 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': 'true'}
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'cabled': 'false'}
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
@@ -2524,10 +2618,17 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@@ -2678,12 +2779,6 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'label': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_enabled(self):
|
||||
params = {'enabled': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
@@ -2793,6 +2888,13 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
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)
|
||||
@@ -2810,9 +2912,21 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': 'true'}
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'cabled': 'false'}
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_kind(self):
|
||||
@@ -2899,10 +3013,17 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@@ -2994,6 +3115,13 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
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]}
|
||||
@@ -3007,9 +3135,15 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': 'true'}
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'cabled': 'false'}
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
@@ -3055,10 +3189,17 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@@ -3144,6 +3285,13 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
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]}
|
||||
@@ -3157,9 +3305,15 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': 'true'}
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'cabled': 'false'}
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
@@ -3204,10 +3358,17 @@ class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -3258,6 +3419,13 @@ class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
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]}
|
||||
@@ -3307,10 +3475,17 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -3361,6 +3536,13 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
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]}
|
||||
@@ -3416,10 +3598,17 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@@ -3503,6 +3692,13 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_device(self):
|
||||
devices = Device.objects.all()[:2]
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
@@ -4019,9 +4215,9 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cabled(self):
|
||||
params = {'cabled': 'true'}
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'cabled': 'false'}
|
||||
params = {'cabled': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_connected(self):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices
|
||||
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
from utilities.testing import create_test_device
|
||||
@@ -129,10 +129,11 @@ class LabelTestCase(TestCase):
|
||||
"""
|
||||
interface_data = {
|
||||
'device': self.device.pk,
|
||||
'name_pattern': 'eth[0-9]',
|
||||
'label_pattern': 'Interface[0-9]',
|
||||
'name': 'eth[0-9]',
|
||||
'label': 'Interface[0-9]',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
}
|
||||
form = DeviceComponentCreateForm(interface_data)
|
||||
form = InterfaceCreateForm(interface_data)
|
||||
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
@@ -142,10 +143,11 @@ class LabelTestCase(TestCase):
|
||||
"""
|
||||
bad_interface_data = {
|
||||
'device': self.device.pk,
|
||||
'name_pattern': 'eth[0-9]',
|
||||
'label_pattern': 'Interface[0-1]',
|
||||
'name': 'eth[0-9]',
|
||||
'label': 'Interface[0-1]',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
}
|
||||
form = DeviceComponentCreateForm(bad_interface_data)
|
||||
form = InterfaceCreateForm(bad_interface_data)
|
||||
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('label_pattern', form.errors)
|
||||
self.assertIn('label', form.errors)
|
||||
|
||||
@@ -1082,31 +1082,28 @@ front-ports:
|
||||
|
||||
class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = ConsolePortTemplate
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
|
||||
ConsolePortTemplate.objects.bulk_create((
|
||||
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 1'),
|
||||
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 2'),
|
||||
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 3'),
|
||||
ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'),
|
||||
ConsolePortTemplate(device_type=devicetype, name='Console Port Template 2'),
|
||||
ConsolePortTemplate(device_type=devicetype, name='Console Port Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Console Port Template X',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Console Port Template [4-6]',
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Console Port Template [4-6]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
@@ -1117,31 +1114,28 @@ class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
|
||||
|
||||
class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = ConsoleServerPortTemplate
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
|
||||
ConsoleServerPortTemplate.objects.bulk_create((
|
||||
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 1'),
|
||||
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 2'),
|
||||
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 3'),
|
||||
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'),
|
||||
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 2'),
|
||||
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Console Server Port Template X',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Console Server Port Template [4-6]',
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Console Server Port Template [4-6]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
@@ -1152,24 +1146,21 @@ class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateVie
|
||||
|
||||
class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = PowerPortTemplate
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
|
||||
PowerPortTemplate.objects.bulk_create((
|
||||
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 1'),
|
||||
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 2'),
|
||||
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 3'),
|
||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
|
||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'),
|
||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Power Port Template X',
|
||||
'type': PowerPortTypeChoices.TYPE_IEC_C14,
|
||||
'maximum_draw': 100,
|
||||
@@ -1177,8 +1168,8 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Power Port Template [4-6]',
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Power Port Template [4-6]',
|
||||
'type': PowerPortTypeChoices.TYPE_IEC_C14,
|
||||
'maximum_draw': 100,
|
||||
'allocated_draw': 50,
|
||||
@@ -1193,6 +1184,7 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
||||
|
||||
class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = PowerOutletTemplate
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1220,7 +1212,7 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetype.pk,
|
||||
'name_pattern': 'Power Outlet Template [4-6]',
|
||||
'name': 'Power Outlet Template [4-6]',
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
|
||||
'power_port': powerports[0].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
@@ -1234,34 +1226,31 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
|
||||
|
||||
class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = InterfaceTemplate
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
|
||||
InterfaceTemplate.objects.bulk_create((
|
||||
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 1'),
|
||||
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 2'),
|
||||
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 3'),
|
||||
InterfaceTemplate(device_type=devicetype, name='Interface Template 1'),
|
||||
InterfaceTemplate(device_type=devicetype, name='Interface Template 2'),
|
||||
InterfaceTemplate(device_type=devicetype, name='Interface Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Interface Template X',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'mgmt_only': True,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Interface Template [4-6]',
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Interface Template [4-6]',
|
||||
# Test that a label can be applied to each generated interface templates
|
||||
'label_pattern': 'Interface Template Label [3-5]',
|
||||
'label': 'Interface Template Label [3-5]',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'mgmt_only': True,
|
||||
}
|
||||
@@ -1274,6 +1263,7 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
||||
|
||||
class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = FrontPortTemplate
|
||||
validation_excluded_fields = ('name', 'label', 'rear_port')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1306,11 +1296,9 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetype.pk,
|
||||
'name_pattern': 'Front Port [4-6]',
|
||||
'name': 'Front Port [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port_set': [
|
||||
'{}:1'.format(rp.pk) for rp in rearports[3:6]
|
||||
],
|
||||
'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -1320,32 +1308,29 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
||||
|
||||
class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = RearPortTemplate
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
|
||||
RearPortTemplate.objects.bulk_create((
|
||||
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 1'),
|
||||
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 2'),
|
||||
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 3'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Rear Port Template X',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'positions': 2,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Rear Port Template [4-6]',
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Rear Port Template [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'positions': 2,
|
||||
}
|
||||
@@ -1357,30 +1342,27 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
|
||||
|
||||
class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = ModuleBayTemplate
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
|
||||
ModuleBayTemplate.objects.bulk_create((
|
||||
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 1'),
|
||||
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 2'),
|
||||
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 3'),
|
||||
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 1'),
|
||||
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2'),
|
||||
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Module Bay Template X',
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Module Bay Template [4-6]',
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Module Bay Template [4-6]',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -1390,30 +1372,27 @@ class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
||||
|
||||
class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = DeviceBayTemplate
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
|
||||
|
||||
DeviceBayTemplate.objects.bulk_create((
|
||||
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 1'),
|
||||
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 2'),
|
||||
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 3'),
|
||||
DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 1'),
|
||||
DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 2'),
|
||||
DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Device Bay Template X',
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Device Bay Template [4-6]',
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Device Bay Template [4-6]',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
@@ -1423,6 +1402,7 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
||||
|
||||
class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = InventoryItemTemplate
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1431,30 +1411,25 @@ class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes
|
||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||
)
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturers[0], model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1')
|
||||
|
||||
inventory_item_templates = (
|
||||
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 1', manufacturer=manufacturers[0]),
|
||||
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 2', manufacturer=manufacturers[0]),
|
||||
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 3', manufacturer=manufacturers[0]),
|
||||
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 1', manufacturer=manufacturers[0]),
|
||||
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 2', manufacturer=manufacturers[0]),
|
||||
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 3', manufacturer=manufacturers[0]),
|
||||
)
|
||||
for item in inventory_item_templates:
|
||||
item.save()
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Inventory Item Template X',
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Inventory Item Template [4-6]',
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Inventory Item Template [4-6]',
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
}
|
||||
|
||||
@@ -1803,10 +1778,12 @@ class ModuleTestCase(
|
||||
ModuleBay(device=devices[0], name='Module Bay 2'),
|
||||
ModuleBay(device=devices[0], name='Module Bay 3'),
|
||||
ModuleBay(device=devices[0], name='Module Bay 4'),
|
||||
ModuleBay(device=devices[0], name='Module Bay 5'),
|
||||
ModuleBay(device=devices[1], name='Module Bay 1'),
|
||||
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||
ModuleBay(device=devices[1], name='Module Bay 3'),
|
||||
ModuleBay(device=devices[1], name='Module Bay 4'),
|
||||
ModuleBay(device=devices[1], name='Module Bay 5'),
|
||||
)
|
||||
ModuleBay.objects.bulk_create(module_bays)
|
||||
|
||||
@@ -1820,7 +1797,7 @@ class ModuleTestCase(
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'device': devices[1].pk,
|
||||
'device': devices[0].pk,
|
||||
'module_bay': module_bays[3].pk,
|
||||
'module_type': module_types[0].pk,
|
||||
'serial': 'A',
|
||||
@@ -1892,7 +1869,6 @@ class ModuleTestCase(
|
||||
self.assertIsNone(interface.module)
|
||||
|
||||
# Create a module with adopted components
|
||||
form_data['module_bay'] = ModuleBay.objects.filter(device=device).first()
|
||||
form_data['module_type'] = module_type
|
||||
form_data['replicate_components'] = False
|
||||
form_data['adopt_components'] = True
|
||||
@@ -1912,6 +1888,7 @@ class ModuleTestCase(
|
||||
|
||||
class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = ConsolePort
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1935,9 +1912,9 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Console Port [4-6]',
|
||||
'name': 'Console Port [4-6]',
|
||||
# Test that a label can be applied to each generated console ports
|
||||
'label_pattern': 'Serial[3-5]',
|
||||
'label': 'Serial[3-5]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console port',
|
||||
'tags': sorted([t.pk for t in tags]),
|
||||
@@ -1970,6 +1947,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = ConsoleServerPort
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1993,7 +1971,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Console Server Port [4-6]',
|
||||
'name': 'Console Server Port [4-6]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console server port',
|
||||
'tags': [t.pk for t in tags],
|
||||
@@ -2026,6 +2004,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = PowerPort
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2051,7 +2030,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Power Port [4-6]]',
|
||||
'name': 'Power Port [4-6]]',
|
||||
'type': PowerPortTypeChoices.TYPE_IEC_C14,
|
||||
'maximum_draw': 100,
|
||||
'allocated_draw': 50,
|
||||
@@ -2088,6 +2067,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = PowerOutlet
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2119,7 +2099,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Power Outlet [4-6]',
|
||||
'name': 'Power Outlet [4-6]',
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
|
||||
'power_port': powerports[1].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
@@ -2153,6 +2133,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = Interface
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2217,7 +2198,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Interface [4-6]',
|
||||
'name': 'Interface [4-6]',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'enabled': False,
|
||||
'bridge': interfaces[4].pk,
|
||||
@@ -2277,6 +2258,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = FrontPort
|
||||
validation_excluded_fields = ('name', 'label', 'rear_port')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2312,11 +2294,9 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Front Port [4-6]',
|
||||
'name': 'Front Port [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port_set': [
|
||||
'{}:1'.format(rp.pk) for rp in rearports[3:6]
|
||||
],
|
||||
'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
|
||||
'description': 'New description',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
@@ -2348,6 +2328,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = RearPort
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2372,7 +2353,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Rear Port [4-6]',
|
||||
'name': 'Rear Port [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'positions': 3,
|
||||
'description': 'A rear port',
|
||||
@@ -2406,6 +2387,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = ModuleBay
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2428,7 +2410,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Module Bay [4-6]',
|
||||
'name': 'Module Bay [4-6]',
|
||||
'description': 'A module bay',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
@@ -2447,6 +2429,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = DeviceBay
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2472,7 +2455,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Device Bay [4-6]',
|
||||
'name': 'Device Bay [4-6]',
|
||||
'description': 'A device bay',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
@@ -2491,6 +2474,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = InventoryItem
|
||||
validation_excluded_fields = ('name', 'label')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2525,7 +2509,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Inventory Item [4-6]',
|
||||
'name': 'Inventory Item [4-6]',
|
||||
'role': roles[1].pk,
|
||||
'manufacturer': manufacturer.pk,
|
||||
'parent': None,
|
||||
|
||||
@@ -355,7 +355,7 @@ class SiteView(generic.ObjectView):
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
site=instance,
|
||||
position__isnull=True,
|
||||
rack__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||
|
||||
@@ -450,7 +450,7 @@ class LocationView(generic.ObjectView):
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
location=instance,
|
||||
position__isnull=True,
|
||||
rack__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||
|
||||
@@ -589,10 +589,17 @@ class RackElevationListView(generic.ObjectListView):
|
||||
racks = filtersets.RackFilterSet(request.GET, self.queryset).qs
|
||||
total_count = racks.count()
|
||||
|
||||
# Determine ordering
|
||||
reverse = bool(request.GET.get('reverse', False))
|
||||
if reverse:
|
||||
racks = racks.reverse()
|
||||
ORDERING_CHOICES = {
|
||||
'name': 'Name (A-Z)',
|
||||
'-name': 'Name (Z-A)',
|
||||
'facility_id': 'Facility ID (A-Z)',
|
||||
'-facility_id': 'Facility ID (Z-A)',
|
||||
}
|
||||
sort = request.GET.get('sort', "name")
|
||||
if sort not in ORDERING_CHOICES:
|
||||
sort = 'name'
|
||||
|
||||
racks = racks.order_by(sort)
|
||||
|
||||
# Pagination
|
||||
per_page = get_paginate_count(request)
|
||||
@@ -614,7 +621,9 @@ class RackElevationListView(generic.ObjectListView):
|
||||
'paginator': paginator,
|
||||
'page': page,
|
||||
'total_count': total_count,
|
||||
'reverse': reverse,
|
||||
'sort': sort,
|
||||
'sort_display_name': ORDERING_CHOICES[sort],
|
||||
'sort_choices': ORDERING_CHOICES,
|
||||
'rack_face': rack_face,
|
||||
'filter_form': forms.RackElevationFilterForm(request.GET),
|
||||
})
|
||||
@@ -1111,9 +1120,8 @@ class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsolePortTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsolePortTemplate.objects.all()
|
||||
form = forms.ModularComponentTemplateCreateForm
|
||||
form = forms.ConsolePortTemplateCreateForm
|
||||
model_form = forms.ConsolePortTemplateForm
|
||||
template_name = 'dcim/component_template_create.html'
|
||||
|
||||
|
||||
class ConsolePortTemplateEditView(generic.ObjectEditView):
|
||||
@@ -1146,9 +1154,8 @@ class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsoleServerPortTemplate.objects.all()
|
||||
form = forms.ModularComponentTemplateCreateForm
|
||||
form = forms.ConsoleServerPortTemplateCreateForm
|
||||
model_form = forms.ConsoleServerPortTemplateForm
|
||||
template_name = 'dcim/component_template_create.html'
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateEditView(generic.ObjectEditView):
|
||||
@@ -1181,9 +1188,8 @@ class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerPortTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerPortTemplate.objects.all()
|
||||
form = forms.ModularComponentTemplateCreateForm
|
||||
form = forms.PowerPortTemplateCreateForm
|
||||
model_form = forms.PowerPortTemplateForm
|
||||
template_name = 'dcim/component_template_create.html'
|
||||
|
||||
|
||||
class PowerPortTemplateEditView(generic.ObjectEditView):
|
||||
@@ -1216,9 +1222,8 @@ class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerOutletTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerOutletTemplate.objects.all()
|
||||
form = forms.ModularComponentTemplateCreateForm
|
||||
form = forms.PowerOutletTemplateCreateForm
|
||||
model_form = forms.PowerOutletTemplateForm
|
||||
template_name = 'dcim/component_template_create.html'
|
||||
|
||||
|
||||
class PowerOutletTemplateEditView(generic.ObjectEditView):
|
||||
@@ -1251,9 +1256,8 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class InterfaceTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = InterfaceTemplate.objects.all()
|
||||
form = forms.ModularComponentTemplateCreateForm
|
||||
form = forms.InterfaceTemplateCreateForm
|
||||
model_form = forms.InterfaceTemplateForm
|
||||
template_name = 'dcim/component_template_create.html'
|
||||
|
||||
|
||||
class InterfaceTemplateEditView(generic.ObjectEditView):
|
||||
@@ -1288,15 +1292,6 @@ class FrontPortTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = FrontPortTemplate.objects.all()
|
||||
form = forms.FrontPortTemplateCreateForm
|
||||
model_form = forms.FrontPortTemplateForm
|
||||
template_name = 'dcim/frontporttemplate_create.html'
|
||||
|
||||
def initialize_forms(self, request):
|
||||
form, model_form = super().initialize_forms(request)
|
||||
|
||||
model_form.fields.pop('rear_port')
|
||||
model_form.fields.pop('rear_port_position')
|
||||
|
||||
return form, model_form
|
||||
|
||||
|
||||
class FrontPortTemplateEditView(generic.ObjectEditView):
|
||||
@@ -1329,9 +1324,8 @@ class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class RearPortTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = RearPortTemplate.objects.all()
|
||||
form = forms.ModularComponentTemplateCreateForm
|
||||
form = forms.RearPortTemplateCreateForm
|
||||
model_form = forms.RearPortTemplateForm
|
||||
template_name = 'dcim/component_template_create.html'
|
||||
|
||||
|
||||
class RearPortTemplateEditView(generic.ObjectEditView):
|
||||
@@ -1366,8 +1360,6 @@ class ModuleBayTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = ModuleBayTemplate.objects.all()
|
||||
form = forms.ModuleBayTemplateCreateForm
|
||||
model_form = forms.ModuleBayTemplateForm
|
||||
template_name = 'dcim/modulebaytemplate_create.html'
|
||||
patterned_fields = ('name', 'label', 'position')
|
||||
|
||||
|
||||
class ModuleBayTemplateEditView(generic.ObjectEditView):
|
||||
@@ -1400,9 +1392,8 @@ class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class DeviceBayTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = DeviceBayTemplate.objects.all()
|
||||
form = forms.ComponentTemplateCreateForm
|
||||
form = forms.DeviceBayTemplateCreateForm
|
||||
model_form = forms.DeviceBayTemplateForm
|
||||
template_name = 'dcim/component_template_create.html'
|
||||
|
||||
|
||||
class DeviceBayTemplateEditView(generic.ObjectEditView):
|
||||
@@ -1435,9 +1426,8 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class InventoryItemTemplateCreateView(generic.ComponentCreateView):
|
||||
queryset = InventoryItemTemplate.objects.all()
|
||||
form = forms.ModularComponentTemplateCreateForm
|
||||
form = forms.InventoryItemTemplateCreateForm
|
||||
model_form = forms.InventoryItemTemplateForm
|
||||
template_name = 'dcim/inventoryitemtemplate_create.html'
|
||||
|
||||
def alter_object(self, instance, request):
|
||||
# Set component (if any)
|
||||
@@ -1626,6 +1616,7 @@ class DeviceView(generic.ObjectView):
|
||||
return {
|
||||
'services': services,
|
||||
'vc_members': vc_members,
|
||||
'svg_extra': f'highlight=id:{instance.pk}'
|
||||
}
|
||||
|
||||
|
||||
@@ -1865,14 +1856,13 @@ class ConsolePortView(generic.ObjectView):
|
||||
|
||||
class ConsolePortCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
form = forms.DeviceComponentCreateForm
|
||||
form = forms.ConsolePortCreateForm
|
||||
model_form = forms.ConsolePortForm
|
||||
|
||||
|
||||
class ConsolePortEditView(generic.ObjectEditView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
form = forms.ConsolePortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class ConsolePortDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1924,14 +1914,13 @@ class ConsoleServerPortView(generic.ObjectView):
|
||||
|
||||
class ConsoleServerPortCreateView(generic.ComponentCreateView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
form = forms.DeviceComponentCreateForm
|
||||
form = forms.ConsoleServerPortCreateForm
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
|
||||
|
||||
class ConsoleServerPortEditView(generic.ObjectEditView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
form = forms.ConsoleServerPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class ConsoleServerPortDeleteView(generic.ObjectDeleteView):
|
||||
@@ -1983,14 +1972,13 @@ class PowerPortView(generic.ObjectView):
|
||||
|
||||
class PowerPortCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerPort.objects.all()
|
||||
form = forms.DeviceComponentCreateForm
|
||||
form = forms.PowerPortCreateForm
|
||||
model_form = forms.PowerPortForm
|
||||
|
||||
|
||||
class PowerPortEditView(generic.ObjectEditView):
|
||||
queryset = PowerPort.objects.all()
|
||||
form = forms.PowerPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class PowerPortDeleteView(generic.ObjectDeleteView):
|
||||
@@ -2042,14 +2030,13 @@ class PowerOutletView(generic.ObjectView):
|
||||
|
||||
class PowerOutletCreateView(generic.ComponentCreateView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
form = forms.DeviceComponentCreateForm
|
||||
form = forms.PowerOutletCreateForm
|
||||
model_form = forms.PowerOutletForm
|
||||
|
||||
|
||||
class PowerOutletEditView(generic.ObjectEditView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
form = forms.PowerOutletForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class PowerOutletDeleteView(generic.ObjectDeleteView):
|
||||
@@ -2145,42 +2132,13 @@ class InterfaceView(generic.ObjectView):
|
||||
|
||||
class InterfaceCreateView(generic.ComponentCreateView):
|
||||
queryset = Interface.objects.all()
|
||||
form = forms.DeviceComponentCreateForm
|
||||
form = forms.InterfaceCreateForm
|
||||
model_form = forms.InterfaceForm
|
||||
# template_name = 'dcim/interface_create.html'
|
||||
|
||||
# TODO: Figure out what to do with this
|
||||
# def post(self, request):
|
||||
# """
|
||||
# Override inherited post() method to handle request to assign newly created
|
||||
# interface objects (first object) to an IP Address object.
|
||||
# """
|
||||
# form = self.form(request.POST, initial=request.GET)
|
||||
# new_objs = self.validate_form(request, form)
|
||||
#
|
||||
# if form.is_valid() and not form.errors:
|
||||
# if '_addanother' in request.POST:
|
||||
# return redirect(request.get_full_path())
|
||||
# elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and \
|
||||
# request.user.has_perm('ipam.add_ipaddress'):
|
||||
# first_obj = new_objs[0].pk
|
||||
# return redirect(
|
||||
# f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}'
|
||||
# )
|
||||
# else:
|
||||
# return redirect(self.get_return_url(request))
|
||||
#
|
||||
# return render(request, self.template_name, {
|
||||
# 'obj_type': self.queryset.model._meta.verbose_name,
|
||||
# 'form': form,
|
||||
# 'return_url': self.get_return_url(request),
|
||||
# })
|
||||
|
||||
|
||||
class InterfaceEditView(generic.ObjectEditView):
|
||||
queryset = Interface.objects.all()
|
||||
form = forms.InterfaceForm
|
||||
template_name = 'dcim/interface_edit.html'
|
||||
|
||||
|
||||
class InterfaceDeleteView(generic.ObjectDeleteView):
|
||||
@@ -2235,19 +2193,10 @@ class FrontPortCreateView(generic.ComponentCreateView):
|
||||
form = forms.FrontPortCreateForm
|
||||
model_form = forms.FrontPortForm
|
||||
|
||||
def initialize_forms(self, request):
|
||||
form, model_form = super().initialize_forms(request)
|
||||
|
||||
model_form.fields.pop('rear_port')
|
||||
model_form.fields.pop('rear_port_position')
|
||||
|
||||
return form, model_form
|
||||
|
||||
|
||||
class FrontPortEditView(generic.ObjectEditView):
|
||||
queryset = FrontPort.objects.all()
|
||||
form = forms.FrontPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class FrontPortDeleteView(generic.ObjectDeleteView):
|
||||
@@ -2299,14 +2248,13 @@ class RearPortView(generic.ObjectView):
|
||||
|
||||
class RearPortCreateView(generic.ComponentCreateView):
|
||||
queryset = RearPort.objects.all()
|
||||
form = forms.DeviceComponentCreateForm
|
||||
form = forms.RearPortCreateForm
|
||||
model_form = forms.RearPortForm
|
||||
|
||||
|
||||
class RearPortEditView(generic.ObjectEditView):
|
||||
queryset = RearPort.objects.all()
|
||||
form = forms.RearPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class RearPortDeleteView(generic.ObjectDeleteView):
|
||||
@@ -2360,13 +2308,11 @@ class ModuleBayCreateView(generic.ComponentCreateView):
|
||||
queryset = ModuleBay.objects.all()
|
||||
form = forms.ModuleBayCreateForm
|
||||
model_form = forms.ModuleBayForm
|
||||
patterned_fields = ('name', 'label', 'position')
|
||||
|
||||
|
||||
class ModuleBayEditView(generic.ObjectEditView):
|
||||
queryset = ModuleBay.objects.all()
|
||||
form = forms.ModuleBayForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class ModuleBayDeleteView(generic.ObjectDeleteView):
|
||||
@@ -2414,14 +2360,13 @@ class DeviceBayView(generic.ObjectView):
|
||||
|
||||
class DeviceBayCreateView(generic.ComponentCreateView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
form = forms.DeviceComponentCreateForm
|
||||
form = forms.DeviceBayCreateForm
|
||||
model_form = forms.DeviceBayForm
|
||||
|
||||
|
||||
class DeviceBayEditView(generic.ObjectEditView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
form = forms.DeviceBayForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class DeviceBayDeleteView(generic.ObjectDeleteView):
|
||||
@@ -2543,7 +2488,6 @@ class InventoryItemCreateView(generic.ComponentCreateView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
form = forms.InventoryItemCreateForm
|
||||
model_form = forms.InventoryItemForm
|
||||
template_name = 'dcim/inventoryitem_create.html'
|
||||
|
||||
def alter_object(self, instance, request):
|
||||
# Set component (if any)
|
||||
@@ -2727,7 +2671,6 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
patterned_fields = ('name', 'label', 'position')
|
||||
|
||||
|
||||
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
|
||||
@@ -2893,7 +2836,7 @@ class CableBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
queryset = ConsolePort.objects.filter(_path__isnull=False).order_by('device')
|
||||
queryset = ConsolePort.objects.filter(_path__is_complete=True)
|
||||
filterset = filtersets.ConsoleConnectionFilterSet
|
||||
filterset_form = forms.ConsoleConnectionFilterForm
|
||||
table = tables.ConsoleConnectionTable
|
||||
@@ -2907,7 +2850,7 @@ class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class PowerConnectionsListView(generic.ObjectListView):
|
||||
queryset = PowerPort.objects.filter(_path__isnull=False).order_by('device')
|
||||
queryset = PowerPort.objects.filter(_path__is_complete=True)
|
||||
filterset = filtersets.PowerConnectionFilterSet
|
||||
filterset_form = forms.PowerConnectionFilterForm
|
||||
table = tables.PowerConnectionTable
|
||||
@@ -2921,7 +2864,7 @@ class PowerConnectionsListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class InterfaceConnectionsListView(generic.ObjectListView):
|
||||
queryset = Interface.objects.filter(_path__isnull=False).order_by('device')
|
||||
queryset = Interface.objects.filter(_path__is_complete=True)
|
||||
filterset = filtersets.InterfaceConnectionFilterSet
|
||||
filterset_form = forms.InterfaceConnectionFilterForm
|
||||
table = tables.InterfaceConnectionTable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework.fields import Field
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import CustomField
|
||||
@@ -62,6 +63,12 @@ class CustomFieldsDataField(Field):
|
||||
return data
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if type(data) is not dict:
|
||||
raise ValidationError(
|
||||
"Invalid data format. Custom field data must be passed as a dictionary mapping field names to their "
|
||||
"values."
|
||||
)
|
||||
|
||||
# If updating an existing instance, start with existing custom_field_data
|
||||
if self.parent.instance:
|
||||
data = {**self.parent.instance.custom_field_data, **data}
|
||||
|
||||
@@ -192,7 +192,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
|
||||
return data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_parent(self, obj):
|
||||
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||
@@ -242,7 +242,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
|
||||
|
||||
return data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_assigned_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
@@ -403,6 +403,7 @@ class ScriptSerializer(serializers.Serializer):
|
||||
vars = serializers.SerializerMethodField(read_only=True)
|
||||
result = NestedJobResultSerializer()
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_vars(self, instance):
|
||||
return {
|
||||
k: v.__class__.__name__ for k, v in instance._get_vars().items()
|
||||
@@ -461,7 +462,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_changed_object(self, obj):
|
||||
"""
|
||||
Serialize a nested representation of the changed object.
|
||||
|
||||
@@ -159,7 +159,7 @@ class ReportViewSet(ViewSet):
|
||||
# Read the PK as "<module>.<report>"
|
||||
if '.' not in pk:
|
||||
raise Http404
|
||||
module_name, report_name = pk.split('.', 1)
|
||||
module_name, report_name = pk.split('.', maxsplit=1)
|
||||
|
||||
# Raise a 404 on an invalid Report module/name
|
||||
report = get_report(module_name, report_name)
|
||||
@@ -183,8 +183,8 @@ class ReportViewSet(ViewSet):
|
||||
}
|
||||
|
||||
# Iterate through all available Reports.
|
||||
for module_name, reports in get_reports():
|
||||
for report in reports:
|
||||
for module_name, reports in get_reports().items():
|
||||
for report in reports.values():
|
||||
|
||||
# Attach the relevant JobResult (if any) to each Report.
|
||||
report.result = results.get(report.full_name, None)
|
||||
@@ -257,7 +257,7 @@ class ScriptViewSet(ViewSet):
|
||||
lookup_value_regex = '[^/]+' # Allow dots
|
||||
|
||||
def _get_script(self, pk):
|
||||
module_name, script_name = pk.split('.')
|
||||
module_name, script_name = pk.split('.', maxsplit=1)
|
||||
script = get_script(module_name, script_name)
|
||||
if script is None:
|
||||
raise Http404
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
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 netbox import thread_locals
|
||||
from netbox.request_context import set_request
|
||||
from netbox.context import current_request, webhooks_queue
|
||||
from .webhooks import flush_webhooks
|
||||
|
||||
|
||||
@@ -16,27 +12,14 @@ def change_logging(request):
|
||||
|
||||
:param request: WSGIRequest object with a unique `id` set
|
||||
"""
|
||||
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')
|
||||
m2m_changed.connect(handle_changed_object, dispatch_uid='handle_changed_object')
|
||||
pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object')
|
||||
clear_webhooks.connect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
|
||||
current_request.set(request)
|
||||
webhooks_queue.set([])
|
||||
|
||||
yield
|
||||
|
||||
# Disconnect change logging signals. This is necessary to avoid recording any errant
|
||||
# changes during test cleanup.
|
||||
post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
|
||||
m2m_changed.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
|
||||
pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object')
|
||||
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
|
||||
|
||||
# Flush queued webhooks to RQ
|
||||
flush_webhooks(thread_locals.webhook_queue)
|
||||
del thread_locals.webhook_queue
|
||||
flush_webhooks(webhooks_queue.get())
|
||||
|
||||
# Clear the request from thread-local storage
|
||||
set_request(None)
|
||||
# Clear context vars
|
||||
current_request.set(None)
|
||||
webhooks_queue.set([])
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.forms import SimpleArrayField
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
|
||||
@@ -38,6 +38,10 @@ class CustomFieldCSVForm(CSVModelForm):
|
||||
required=False,
|
||||
help_text='Comma-separated list of field choices'
|
||||
)
|
||||
ui_visibility = CSVChoiceField(
|
||||
choices=CustomFieldVisibilityChoices,
|
||||
help_text='How the custom field is displayed in the user interface'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
|
||||
@@ -34,7 +34,9 @@ class CustomFieldsMixin:
|
||||
return ContentType.objects.get_for_model(self.model)
|
||||
|
||||
def _get_custom_fields(self, content_type):
|
||||
return CustomField.objects.filter(content_types=content_type)
|
||||
return CustomField.objects.filter(content_types=content_type).exclude(
|
||||
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
|
||||
)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field()
|
||||
@@ -50,13 +52,6 @@ class CustomFieldsMixin:
|
||||
field_name = f'cf_{customfield.name}'
|
||||
self.fields[field_name] = self._get_form_field(customfield)
|
||||
|
||||
if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
||||
self.fields[field_name].disabled = True
|
||||
if self.fields[field_name].help_text:
|
||||
self.fields[field_name].help_text += '<br />'
|
||||
self.fields[field_name].help_text += '<i class="mdi mdi-alert-circle-outline"></i> ' \
|
||||
'Field is set to read-only.'
|
||||
|
||||
# Annotate the field in the list of CustomField form fields
|
||||
self.custom_fields[field_name] = customfield
|
||||
if customfield.group_name not in self.custom_field_groups:
|
||||
|
||||
@@ -21,8 +21,8 @@ class Command(BaseCommand):
|
||||
reports = get_reports()
|
||||
|
||||
# Run reports
|
||||
for module_name, report_list in reports:
|
||||
for report in report_list:
|
||||
for module_name, report_list in reports.items():
|
||||
for report in report_list.values():
|
||||
if module_name in options['reports'] or report.full_name in options['reports']:
|
||||
|
||||
# Run the report and create a new JobResult
|
||||
|
||||
@@ -14,7 +14,7 @@ from django.utils.safestring import mark_safe
|
||||
from extras.choices import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
|
||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin
|
||||
from utilities import filters
|
||||
from utilities.forms import (
|
||||
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
@@ -41,7 +41,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
||||
return self.get_queryset().filter(content_types=content_type)
|
||||
|
||||
|
||||
class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
content_types = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
related_name='custom_fields',
|
||||
@@ -143,8 +143,14 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
verbose_name='UI visibility',
|
||||
help_text='Specifies the visibility of custom field in the UI'
|
||||
)
|
||||
|
||||
objects = CustomFieldManager()
|
||||
|
||||
clone_fields = (
|
||||
'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'filter_logic', 'default',
|
||||
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'ui_visibility',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['group_name', 'weight', 'name']
|
||||
|
||||
@@ -291,12 +297,13 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
return model.objects.filter(pk__in=value)
|
||||
return value
|
||||
|
||||
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
|
||||
def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibility=True, for_csv_import=False):
|
||||
"""
|
||||
Return a form field suitable for setting a CustomField's value for an object.
|
||||
|
||||
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
|
||||
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
||||
enforce_visibility: Honor the value of CustomField.ui_visibility. Set to False for filtering.
|
||||
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
||||
"""
|
||||
initial = self.default if set_initial else None
|
||||
@@ -392,6 +399,12 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
if self.description:
|
||||
field.help_text = escape(self.description)
|
||||
|
||||
# Annotate read-only fields
|
||||
if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
||||
field.disabled = True
|
||||
prepend = '<br />' if field.help_text else ''
|
||||
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> Field is set to read-only.'
|
||||
|
||||
return field
|
||||
|
||||
def to_filter(self, lookup_expr=None):
|
||||
|
||||
@@ -21,7 +21,7 @@ from extras.conditions import ConditionSet
|
||||
from extras.utils import FeatureQuery, image_upload
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import (
|
||||
CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
|
||||
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
|
||||
)
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import render_jinja2
|
||||
@@ -187,7 +187,7 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
return render_jinja2(self.payload_url, context)
|
||||
|
||||
|
||||
class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
"""
|
||||
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
|
||||
code to be rendered with an object as context.
|
||||
@@ -230,6 +230,10 @@ class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
help_text="Force link to open in a new window"
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['group_name', 'weight', 'name']
|
||||
|
||||
@@ -459,6 +463,14 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:journalentry', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Prevent the creation of journal entries on unsupported models
|
||||
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
|
||||
if self.assigned_object_type not in permitted_types:
|
||||
raise ValidationError(f"Journaling is not supported for this object type ({self.assigned_object_type}).")
|
||||
|
||||
def get_kind_color(self):
|
||||
return JournalEntryKindChoices.colors.get(self.kind)
|
||||
|
||||
|
||||
@@ -26,20 +26,18 @@ def get_report(module_name, report_name):
|
||||
"""
|
||||
Return a specific report from within a module.
|
||||
"""
|
||||
file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name)
|
||||
reports = get_reports()
|
||||
module = reports.get(module_name)
|
||||
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
except FileNotFoundError:
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
report = getattr(module, report_name, None)
|
||||
report = module.get(report_name)
|
||||
|
||||
if report is None:
|
||||
return None
|
||||
|
||||
return report()
|
||||
return report
|
||||
|
||||
|
||||
def get_reports():
|
||||
@@ -52,7 +50,7 @@ def get_reports():
|
||||
...
|
||||
]
|
||||
"""
|
||||
module_list = []
|
||||
module_list = {}
|
||||
|
||||
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
||||
# defined.
|
||||
@@ -61,7 +59,16 @@ def get_reports():
|
||||
report_order = getattr(module, "report_order", ())
|
||||
ordered_reports = [cls() for cls in report_order if is_report(cls)]
|
||||
unordered_reports = [cls() for _, cls in inspect.getmembers(module, is_report) if cls not in report_order]
|
||||
module_list.append((module_name, [*ordered_reports, *unordered_reports]))
|
||||
|
||||
module_reports = {}
|
||||
|
||||
for cls in [*ordered_reports, *unordered_reports]:
|
||||
# For reports in submodules use the full import path w/o the root module as the name
|
||||
report_name = cls.full_name.split(".", maxsplit=1)[1]
|
||||
module_reports[report_name] = cls
|
||||
|
||||
if module_reports:
|
||||
module_list[module_name] = module_reports
|
||||
|
||||
return module_list
|
||||
|
||||
|
||||
@@ -299,6 +299,10 @@ class BaseScript:
|
||||
def module(cls):
|
||||
return cls.__module__
|
||||
|
||||
@classmethod
|
||||
def root_module(cls):
|
||||
return cls.__module__.split(".")[0]
|
||||
|
||||
@classproperty
|
||||
def job_timeout(self):
|
||||
return getattr(self.Meta, 'job_timeout', None)
|
||||
@@ -514,7 +518,9 @@ def get_scripts(use_names=False):
|
||||
ordered_scripts = [cls for cls in script_order if is_script(cls)]
|
||||
unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order]
|
||||
for cls in [*ordered_scripts, *unordered_scripts]:
|
||||
module_scripts[cls.__name__] = cls
|
||||
# For scripts in submodules use the full import path w/o the root module as the name
|
||||
script_name = cls.full_name.split(".", maxsplit=1)[1]
|
||||
module_scripts[script_name] = cls
|
||||
if module_scripts:
|
||||
scripts[module_name] = module_scripts
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ from django.dispatch import receiver, Signal
|
||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||
|
||||
from extras.validators import CustomValidator
|
||||
from netbox import thread_locals
|
||||
from netbox.config import get_config
|
||||
from netbox.request_context import get_request
|
||||
from netbox.context import current_request, webhooks_queue
|
||||
from netbox.signals import post_clean
|
||||
from .choices import ObjectChangeActionChoices
|
||||
from .models import ConfigRevision, CustomField, ObjectChange
|
||||
from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
|
||||
|
||||
|
||||
#
|
||||
# Change logging/webhooks
|
||||
#
|
||||
@@ -23,22 +23,32 @@ from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
|
||||
clear_webhooks = Signal()
|
||||
|
||||
|
||||
def is_same_object(instance, webhook_data, request_id):
|
||||
"""
|
||||
Compare the given instance to the most recent queued webhook object, returning True
|
||||
if they match. This check is used to avoid creating duplicate webhook entries.
|
||||
"""
|
||||
return (
|
||||
ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and
|
||||
instance.pk == webhook_data['object_id'] and
|
||||
request_id == webhook_data['request_id']
|
||||
)
|
||||
|
||||
|
||||
@receiver((post_save, m2m_changed))
|
||||
def handle_changed_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Fires when an object is created or updated.
|
||||
"""
|
||||
m2m_changed = False
|
||||
|
||||
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
|
||||
instance.pk == webhook_data['object_id'] and
|
||||
request.id == webhook_data['request_id']
|
||||
)
|
||||
# Get the current request, or bail if not set
|
||||
request = current_request.get()
|
||||
if request is None:
|
||||
return
|
||||
|
||||
# Determine the type of change being made
|
||||
if kwargs.get('created'):
|
||||
@@ -69,13 +79,14 @@ def handle_changed_object(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]):
|
||||
queue = webhooks_queue.get()
|
||||
if m2m_changed and queue and is_same_object(instance, queue[-1], request.id):
|
||||
instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments
|
||||
webhook_queue[-1]['data'] = serialize_for_webhook(instance)
|
||||
webhook_queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
|
||||
queue[-1]['data'] = serialize_for_webhook(instance)
|
||||
queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
|
||||
else:
|
||||
enqueue_object(webhook_queue, instance, request.user, request.id, action)
|
||||
enqueue_object(queue, instance, request.user, request.id, action)
|
||||
webhooks_queue.set(queue)
|
||||
|
||||
# Increment metric counters
|
||||
if action == ObjectChangeActionChoices.ACTION_CREATE:
|
||||
@@ -84,6 +95,7 @@ def handle_changed_object(sender, instance, **kwargs):
|
||||
model_updates.labels(instance._meta.model_name).inc()
|
||||
|
||||
|
||||
@receiver(pre_delete)
|
||||
def handle_deleted_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Fires when an object is deleted.
|
||||
@@ -91,7 +103,10 @@ def handle_deleted_object(sender, instance, **kwargs):
|
||||
if not hasattr(instance, 'to_objectchange'):
|
||||
return
|
||||
|
||||
request = get_request()
|
||||
# Get the current request, or bail if not set
|
||||
request = current_request.get()
|
||||
if request is None:
|
||||
return
|
||||
|
||||
# Record an ObjectChange if applicable
|
||||
if hasattr(instance, 'to_objectchange'):
|
||||
@@ -101,22 +116,22 @@ def handle_deleted_object(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)
|
||||
queue = webhooks_queue.get()
|
||||
enqueue_object(queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
|
||||
webhooks_queue.set(queue)
|
||||
|
||||
# Increment metric counters
|
||||
model_deletes.labels(instance._meta.model_name).inc()
|
||||
|
||||
|
||||
@receiver(clear_webhooks)
|
||||
def clear_webhook_queue(sender, **kwargs):
|
||||
"""
|
||||
Delete any queued webhooks (e.g. because of an aborted bulk transaction)
|
||||
"""
|
||||
logger = logging.getLogger('webhooks')
|
||||
webhook_queue = thread_locals.webhook_queue
|
||||
|
||||
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
|
||||
webhook_queue.clear()
|
||||
logger.info(f"Clearing {len(webhooks_queue.get())} queued webhooks ({sender})")
|
||||
webhooks_queue.set([])
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -183,6 +183,7 @@ class ObjectChangeTable(NetBoxTable):
|
||||
verbose_name='Username'
|
||||
)
|
||||
full_name = tables.TemplateColumn(
|
||||
accessor=tables.A('user'),
|
||||
template_code=OBJECTCHANGE_FULL_NAME,
|
||||
verbose_name='Full Name',
|
||||
orderable=False
|
||||
@@ -192,6 +193,7 @@ class ObjectChangeTable(NetBoxTable):
|
||||
verbose_name='Type'
|
||||
)
|
||||
object_repr = tables.TemplateColumn(
|
||||
accessor=tables.A('changed_object'),
|
||||
template_code=OBJECTCHANGE_OBJECT,
|
||||
verbose_name='Object'
|
||||
)
|
||||
|
||||
@@ -9,12 +9,12 @@ CONFIGCONTEXT_ACTIONS = """
|
||||
|
||||
OBJECTCHANGE_FULL_NAME = """
|
||||
{% load helpers %}
|
||||
{{ record.user.get_full_name|placeholder }}
|
||||
{{ value.get_full_name|placeholder }}
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_OBJECT = """
|
||||
{% if record.changed_object and record.changed_object.get_absolute_url %}
|
||||
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||
{% if value and value.get_absolute_url %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||
{% else %}
|
||||
{{ record.object_repr }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.urls import path
|
||||
from django.urls import path, re_path
|
||||
|
||||
from extras import models, views
|
||||
from netbox.views.generic import ObjectChangeLogView
|
||||
@@ -100,12 +100,12 @@ urlpatterns = [
|
||||
|
||||
# Reports
|
||||
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
||||
path('reports/<str:module>.<str:name>/', views.ReportView.as_view(), name='report'),
|
||||
path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
|
||||
re_path(r'^reports/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ReportView.as_view(), name='report'),
|
||||
|
||||
# Scripts
|
||||
path('scripts/', views.ScriptListView.as_view(), name='script_list'),
|
||||
path('scripts/<str:module>.<str:name>/', views.ScriptView.as_view(), name='script'),
|
||||
path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
|
||||
re_path(r'^scripts/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ScriptView.as_view(), name='script'),
|
||||
|
||||
]
|
||||
|
||||
@@ -441,6 +441,12 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
||||
def get_return_url(self, request, obj=None):
|
||||
return obj.parent.get_absolute_url() if obj else super().get_return_url(request)
|
||||
|
||||
def get_extra_addanother_params(self, request):
|
||||
return {
|
||||
'content_type': request.GET.get('content_type'),
|
||||
'object_id': request.GET.get('object_id'),
|
||||
}
|
||||
|
||||
|
||||
class ImageAttachmentDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ImageAttachment.objects.all()
|
||||
@@ -528,9 +534,10 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
|
||||
}
|
||||
|
||||
ret = []
|
||||
for module, report_list in reports:
|
||||
|
||||
for module, report_list in reports.items():
|
||||
module_reports = []
|
||||
for report in report_list:
|
||||
for report in report_list.values():
|
||||
report.result = results.get(report.full_name, None)
|
||||
module_reports.append(report)
|
||||
ret.append((module, module_reports))
|
||||
@@ -607,7 +614,7 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View):
|
||||
result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type)
|
||||
|
||||
# Retrieve the Report and attach the JobResult to it
|
||||
module, report_name = result.name.split('.')
|
||||
module, report_name = result.name.split('.', maxsplit=1)
|
||||
report = get_report(module, report_name)
|
||||
report.result = result
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_interface(self, obj):
|
||||
if obj.interface is None:
|
||||
return None
|
||||
@@ -175,6 +175,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
|
||||
queryset=ContentType.objects.filter(
|
||||
model__in=VLANGROUP_SCOPE_TYPES
|
||||
),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
@@ -190,6 +191,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
|
||||
]
|
||||
validators = []
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_scope(self, obj):
|
||||
if obj.scope_id is None:
|
||||
return None
|
||||
@@ -373,7 +375,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_assigned_object(self, obj):
|
||||
if obj.assigned_object is None:
|
||||
return None
|
||||
@@ -482,7 +484,7 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer):
|
||||
'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
def get_assigned_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
|
||||
@@ -112,6 +112,18 @@ class IPAddressViewSet(NetBoxModelViewSet):
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
filterset_class = filtersets.IPAddressFilterSet
|
||||
|
||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
||||
def create(self, request, *args, **kwargs):
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
||||
def update(self, request, *args, **kwargs):
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
class FHRPGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
|
||||
@@ -174,6 +186,21 @@ class L2VPNTerminationViewSet(NetBoxModelViewSet):
|
||||
# Views
|
||||
#
|
||||
|
||||
def get_results_limit(request):
|
||||
"""
|
||||
Return the lesser of the specified limit (if any) and the configured MAX_PAGE_SIZE.
|
||||
"""
|
||||
config = get_config()
|
||||
try:
|
||||
limit = int(request.query_params.get('limit', config.PAGINATE_COUNT)) or config.MAX_PAGE_SIZE
|
||||
except ValueError:
|
||||
limit = config.PAGINATE_COUNT
|
||||
if config.MAX_PAGE_SIZE:
|
||||
limit = min(limit, config.MAX_PAGE_SIZE)
|
||||
|
||||
return limit
|
||||
|
||||
|
||||
class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
||||
queryset = Prefix.objects.all()
|
||||
|
||||
@@ -265,16 +292,7 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
||||
@swagger_auto_schema(responses={200: serializers.AvailableIPSerializer(many=True)})
|
||||
def get(self, request, pk):
|
||||
parent = self.get_parent(request, pk)
|
||||
config = get_config()
|
||||
PAGINATE_COUNT = config.PAGINATE_COUNT
|
||||
MAX_PAGE_SIZE = config.MAX_PAGE_SIZE
|
||||
|
||||
try:
|
||||
limit = int(request.query_params.get('limit', PAGINATE_COUNT))
|
||||
except ValueError:
|
||||
limit = PAGINATE_COUNT
|
||||
if MAX_PAGE_SIZE:
|
||||
limit = min(limit, MAX_PAGE_SIZE)
|
||||
limit = get_results_limit(request)
|
||||
|
||||
# Calculate available IPs within the parent
|
||||
ip_list = []
|
||||
@@ -357,8 +375,9 @@ class AvailableVLANsView(ObjectValidationMixin, APIView):
|
||||
@swagger_auto_schema(responses={200: serializers.AvailableVLANSerializer(many=True)})
|
||||
def get(self, request, pk):
|
||||
vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
|
||||
available_vlans = vlangroup.get_available_vids()
|
||||
limit = get_results_limit(request)
|
||||
|
||||
available_vlans = vlangroup.get_available_vids()[:limit]
|
||||
serializer = serializers.AvailableVLANSerializer(available_vlans, many=True, context={
|
||||
'request': request,
|
||||
'group': vlangroup,
|
||||
|
||||
@@ -965,7 +965,11 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(identifier=value) | Q(name__icontains=value) | Q(description__icontains=value)
|
||||
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||
try:
|
||||
qs_filter |= Q(identifier=int(value))
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
@@ -1071,6 +1075,12 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
||||
qs_filter = Q(l2vpn__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_assigned_object(self, queryset, name, value):
|
||||
qs = queryset.filter(
|
||||
Q(**{'{}__in'.format(name): value})
|
||||
)
|
||||
return qs
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
qs = queryset.filter(
|
||||
Q(
|
||||
|
||||
@@ -478,6 +478,7 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
class ServiceFilterForm(ServiceTemplateFilterForm):
|
||||
model = Service
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
|
||||
@@ -88,6 +88,12 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm):
|
||||
class RIRForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('RIR', (
|
||||
'name', 'slug', 'is_private', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = [
|
||||
@@ -164,6 +170,12 @@ class ASNForm(TenancyForm, NetBoxModelForm):
|
||||
class RoleForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
('Role', (
|
||||
'name', 'slug', 'weight', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = [
|
||||
@@ -537,9 +549,15 @@ class FHRPGroupForm(NetBoxModelForm):
|
||||
fields = (
|
||||
'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags',
|
||||
)
|
||||
widgets = {
|
||||
'protocol': StaticSelect(),
|
||||
'auth_type': StaticSelect(),
|
||||
'ip_status': StaticSelect(),
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
user = getattr(instance, '_user', None) # Set under FHRPGroupEditView.alter_object()
|
||||
|
||||
# Check if we need to create a new IPAddress for the group
|
||||
if self.cleaned_data.get('ip_address'):
|
||||
@@ -553,7 +571,7 @@ class FHRPGroupForm(NetBoxModelForm):
|
||||
ipaddress.save()
|
||||
|
||||
# Check that the new IPAddress conforms with any assigned object-level permissions
|
||||
if not IPAddress.objects.filter(pk=ipaddress.pk).first():
|
||||
if not IPAddress.objects.restrict(user, 'add').filter(pk=ipaddress.pk).first():
|
||||
raise PermissionsViolation()
|
||||
|
||||
return instance
|
||||
@@ -784,6 +802,12 @@ class ServiceTemplateForm(NetBoxModelForm):
|
||||
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Service Template', (
|
||||
'name', 'protocol', 'ports', 'description', 'tags',
|
||||
)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ServiceTemplate
|
||||
fields = ('name', 'protocol', 'ports', 'description', 'tags')
|
||||
@@ -854,6 +878,7 @@ class ServiceCreateForm(ServiceForm):
|
||||
del self.fields[field].widget.attrs['required']
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.cleaned_data['service_template']:
|
||||
# Create a new Service from the specified template
|
||||
service_template = self.cleaned_data['service_template']
|
||||
|
||||
18
netbox/ipam/migrations/0060_alter_l2vpn_slug.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.7 on 2022-08-22 15:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0059_l2vpn'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='l2vpn',
|
||||
name='slug',
|
||||
field=models.SlugField(max_length=100, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -35,13 +35,16 @@ class GetAvailablePrefixesMixin:
|
||||
|
||||
def get_available_prefixes(self):
|
||||
"""
|
||||
Return all available Prefixes within this aggregate as an IPSet.
|
||||
Return all available prefixes within this Aggregate or Prefix as an IPSet.
|
||||
"""
|
||||
prefix = netaddr.IPSet(self.prefix)
|
||||
child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()])
|
||||
available_prefixes = prefix - child_prefixes
|
||||
params = {
|
||||
'prefix__net_contained': str(self.prefix)
|
||||
}
|
||||
if hasattr(self, 'vrf'):
|
||||
params['vrf'] = self.vrf
|
||||
|
||||
return available_prefixes
|
||||
child_prefixes = Prefix.objects.filter(**params).values_list('prefix', flat=True)
|
||||
return netaddr.IPSet(self.prefix) - netaddr.IPSet(child_prefixes)
|
||||
|
||||
def get_first_available_prefix(self):
|
||||
"""
|
||||
@@ -124,6 +127,10 @@ class ASN(NetBoxModel):
|
||||
def __str__(self):
|
||||
return f'AS{self.asn_with_asdot}'
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [RIR, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:asn', args=[self.pk])
|
||||
|
||||
@@ -185,6 +192,10 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel):
|
||||
def __str__(self):
|
||||
return str(self.prefix)
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [RIR, ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:aggregate', args=[self.pk])
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from ipam.choices import L2VPNTypeChoices
|
||||
from ipam.constants import L2VPN_ASSIGNMENT_MODELS
|
||||
@@ -19,7 +21,10 @@ class L2VPN(NetBoxModel):
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
slug = models.SlugField()
|
||||
slug = models.SlugField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=L2VPNTypeChoices
|
||||
@@ -53,6 +58,8 @@ class L2VPN(NetBoxModel):
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
clone_fields = ('type',)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'identifier')
|
||||
verbose_name = 'L2VPN'
|
||||
@@ -65,6 +72,13 @@ class L2VPN(NetBoxModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:l2vpn', args=[self.pk])
|
||||
|
||||
@cached_property
|
||||
def can_add_termination(self):
|
||||
if self.type in L2VPNTypeChoices.P2P and self.terminations.count() >= 2:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class L2VPNTermination(NetBoxModel):
|
||||
l2vpn = models.ForeignKey(
|
||||
@@ -101,6 +115,10 @@ class L2VPNTermination(NetBoxModel):
|
||||
return f'{self.assigned_object} <> {self.l2vpn}'
|
||||
return super().__str__()
|
||||
|
||||
@classmethod
|
||||
def get_prerequisite_models(cls):
|
||||
return [apps.get_model('ipam.L2VPN'), ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:l2vpntermination', args=[self.pk])
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ class Service(ServiceBase, NetBoxModel):
|
||||
verbose_name='IP addresses'
|
||||
)
|
||||
|
||||
clone_fields = ['protocol', 'ports', 'description', 'device', 'virtual_machine', 'ipaddresses', ]
|
||||
|
||||
class Meta:
|
||||
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
|
||||
|
||||
|
||||