Compare commits
283 Commits
v3.7-beta1
...
v3.7.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d115601da3 | ||
|
|
a61e20849b | ||
|
|
1eca1c3d17 | ||
|
|
5d95d49268 | ||
|
|
6b8bfe9947 | ||
|
|
e87877b6ea | ||
|
|
ebe504c825 | ||
|
|
b6e38b2ebe | ||
|
|
90d0104359 | ||
|
|
88facbafbb | ||
|
|
c9de3128ca | ||
|
|
94c31622ac | ||
|
|
3d3c1c315b | ||
|
|
f4c8f5f5b6 | ||
|
|
19fe5ef25c | ||
|
|
928014c766 | ||
|
|
b5bb732031 | ||
|
|
b8cedfcc08 | ||
|
|
c5ae89ad03 | ||
|
|
4284028bb0 | ||
|
|
3c3943c809 | ||
|
|
17e8773c8c | ||
|
|
f47b158863 | ||
|
|
f7e4fe2a9c | ||
|
|
5098422f68 | ||
|
|
d7922a68d8 | ||
|
|
54c6d95fbb | ||
|
|
b7668fbfc3 | ||
|
|
1c76034069 | ||
|
|
ad0e476788 | ||
|
|
e10f5ec3b4 | ||
|
|
48a3f3cb70 | ||
|
|
238fa704b9 | ||
|
|
3b3511c43c | ||
|
|
da13fa5569 | ||
|
|
5b50920c61 | ||
|
|
d9a7b4ee0e | ||
|
|
282dc7a705 | ||
|
|
e1753c0f9b | ||
|
|
0e94f2e05d | ||
|
|
1c370f45d0 | ||
|
|
24e2fc253a | ||
|
|
fca23c6419 | ||
|
|
e4984d2883 | ||
|
|
6030c521f4 | ||
|
|
83dad6f771 | ||
|
|
bb4930b62f | ||
|
|
7d54357146 | ||
|
|
bbd7ddb7aa | ||
|
|
d285edc0c7 | ||
|
|
699dd72597 | ||
|
|
3cb68e4bc0 | ||
|
|
d37a6210fa | ||
|
|
69c0aac105 | ||
|
|
da6a1ef03e | ||
|
|
d2fee88600 | ||
|
|
710f9e3c46 | ||
|
|
59e12e73c2 | ||
|
|
6f9f1d9d43 | ||
|
|
35e20d156d | ||
|
|
4adb44f60d | ||
|
|
c2cabe0273 | ||
|
|
06bdfdc9e8 | ||
|
|
df7905d257 | ||
|
|
8bdbb49a27 | ||
|
|
7ac21690e5 | ||
|
|
7350950e88 | ||
|
|
8fe3f5e3fd | ||
|
|
44f7ab0970 | ||
|
|
eca2a77584 | ||
|
|
51b2bcf264 | ||
|
|
1ff4e1287f | ||
|
|
f0e137133f | ||
|
|
de622801f1 | ||
|
|
eeb732d96e | ||
|
|
8bb49d2296 | ||
|
|
6629c94148 | ||
|
|
bdcf4c4154 | ||
|
|
c45acf0a7c | ||
|
|
8afbb4421b | ||
|
|
55ef24d56d | ||
|
|
edb7d24b45 | ||
|
|
17ec264f3a | ||
|
|
c21ec2139d | ||
|
|
d7e7137582 | ||
|
|
b7f6b728b9 | ||
|
|
503c78b0db | ||
|
|
cb05288c4d | ||
|
|
0373b8aade | ||
|
|
580d417aa1 | ||
|
|
8571f428b1 | ||
|
|
276a73f820 | ||
|
|
d8fb5a819f | ||
|
|
f14eac58e4 | ||
|
|
1780acc8a6 | ||
|
|
a3b8262ab0 | ||
|
|
a1ee02cdf0 | ||
|
|
f751afcce7 | ||
|
|
17a321a340 | ||
|
|
cf3969bc6c | ||
|
|
9b9afdcf79 | ||
|
|
50e5bb9717 | ||
|
|
a063b5563c | ||
|
|
8678d1a577 | ||
|
|
839609d101 | ||
|
|
dbcd713fe7 | ||
|
|
d216161014 | ||
|
|
056543e1d2 | ||
|
|
af27bf5eff | ||
|
|
29f029d480 | ||
|
|
bd7d4a3f34 | ||
|
|
de5c5aeb2a | ||
|
|
2e74952ac6 | ||
|
|
7cc215437f | ||
|
|
e84e2a7969 | ||
|
|
2d70b50286 | ||
|
|
01fa2710eb | ||
|
|
12d830bcf2 | ||
|
|
c37dfdc150 | ||
|
|
df910928f2 | ||
|
|
1f800a975f | ||
|
|
c7ae2db8e3 | ||
|
|
ae7d6ffd92 | ||
|
|
011bc5bd78 | ||
|
|
040dbcc875 | ||
|
|
64b2ebdc79 | ||
|
|
4afebd3565 | ||
|
|
28aee9b69a | ||
|
|
426805cd24 | ||
|
|
a331ba65cb | ||
|
|
4ba0ec78cf | ||
|
|
93edf74f7c | ||
|
|
8a77ec70f2 | ||
|
|
0eba3acdb8 | ||
|
|
32083e58c0 | ||
|
|
8e8d302850 | ||
|
|
fde9c1664a | ||
|
|
1a9149d7d4 | ||
|
|
31fb6961e9 | ||
|
|
b408beaed5 | ||
|
|
1b6fc49a3e | ||
|
|
9f25289ce2 | ||
|
|
59510b4bd0 | ||
|
|
1b9e6bed55 | ||
|
|
ba755221bb | ||
|
|
b9cac97b73 | ||
|
|
3dc43861c5 | ||
|
|
b9b4b8ae62 | ||
|
|
98c9f7fbbd | ||
|
|
441e24bca7 | ||
|
|
c8fb948a91 | ||
|
|
a141f7f771 | ||
|
|
f26ac3e7cb | ||
|
|
487f1ccfde | ||
|
|
481d16de08 | ||
|
|
23e201cec6 | ||
|
|
fea8efa149 | ||
|
|
0df7ca4309 | ||
|
|
e4188b5bde | ||
|
|
cd8e977418 | ||
|
|
88e4559b5a | ||
|
|
d606749335 | ||
|
|
ff752dac07 | ||
|
|
3aaf370d4a | ||
|
|
fd5392563f | ||
|
|
79e0d3ae67 | ||
|
|
1d15ba56b9 | ||
|
|
2b4ec9dc20 | ||
|
|
1651a307c8 | ||
|
|
93a05289ad | ||
|
|
04575aa0f8 | ||
|
|
d5733a1e89 | ||
|
|
48168de4ff | ||
|
|
a87d76ad17 | ||
|
|
749fc31bc4 | ||
|
|
ebf6ce1b01 | ||
|
|
b871a6c7a6 | ||
|
|
61739a0bc5 | ||
|
|
66db4f3874 | ||
|
|
5de2dea8a6 | ||
|
|
621c3ccfa4 | ||
|
|
530a15e906 | ||
|
|
1235b496b4 | ||
|
|
70dd8f17b6 | ||
|
|
c173c26e35 | ||
|
|
bb806e21f7 | ||
|
|
c5cbb99bf0 | ||
|
|
3d941411d4 | ||
|
|
c4c1ddf68d | ||
|
|
3645bd770f | ||
|
|
0f4c25fe49 | ||
|
|
2221a9d71f | ||
|
|
edc2e3809d | ||
|
|
9603644ca2 | ||
|
|
e1e198ec4f | ||
|
|
5223486fd8 | ||
|
|
ea5d33f358 | ||
|
|
c78a792ccc | ||
|
|
109daca203 | ||
|
|
982ef3045d | ||
|
|
7b90481fc9 | ||
|
|
d99e6510e1 | ||
|
|
7c4b939b59 | ||
|
|
c1ff74894c | ||
|
|
33af942571 | ||
|
|
224484ebb6 | ||
|
|
d9c1ba8972 | ||
|
|
d930c4e36e | ||
|
|
d5c1cb0ef6 | ||
|
|
0c0672550a | ||
|
|
199685d98b | ||
|
|
3ef2db81e8 | ||
|
|
3bacee16bd | ||
|
|
45c646dcec | ||
|
|
fedcbaf4c8 | ||
|
|
359c0cf3a0 | ||
|
|
11bc460551 | ||
|
|
1f2f0860fe | ||
|
|
46b933a5aa | ||
|
|
07da3f6d33 | ||
|
|
4eadc8cfe4 | ||
|
|
0613e8e95c | ||
|
|
113c60a44a | ||
|
|
8a237561ef | ||
|
|
cc0fc03ec3 | ||
|
|
b955751349 | ||
|
|
d6c8d1581c | ||
|
|
e6642b5f5b | ||
|
|
a67236fc3c | ||
|
|
634681a72e | ||
|
|
031b7540b3 | ||
|
|
43909ee33f | ||
|
|
99467e8f66 | ||
|
|
00807d1e52 | ||
|
|
0d08205ab1 | ||
|
|
c289dda649 | ||
|
|
169207058f | ||
|
|
e5c565cbf4 | ||
|
|
f0b9008529 | ||
|
|
8dfec7e2b2 | ||
|
|
c1cf037eaf | ||
|
|
3f4a65cc5c | ||
|
|
58f925c261 | ||
|
|
326b54b7e0 | ||
|
|
3905ddf163 | ||
|
|
3cd2432aa1 | ||
|
|
12beac4f1a | ||
|
|
a233dc91fe | ||
|
|
b794bd6fb8 | ||
|
|
96878cfca6 | ||
|
|
25e67eb555 | ||
|
|
ec245b968f | ||
|
|
f1d4011b40 | ||
|
|
4cdc30a7c5 | ||
|
|
8d39181842 | ||
|
|
3068f2a075 | ||
|
|
224d64007a | ||
|
|
c81869c795 | ||
|
|
929d4d2c95 | ||
|
|
d14e4ab52b | ||
|
|
8a4233aca1 | ||
|
|
5508e125ba | ||
|
|
69bf1472d2 | ||
|
|
b93735861d | ||
|
|
6939ae4a47 | ||
|
|
81fa4265da | ||
|
|
965f2de34b | ||
|
|
35be4f05ef | ||
|
|
d428dd172c | ||
|
|
2ef023a160 | ||
|
|
9d7192202d | ||
|
|
95a8415e2d | ||
|
|
b532435a6d | ||
|
|
2d1f882724 | ||
|
|
e59ee3e01e | ||
|
|
5d2f499ffb | ||
|
|
92bdaa2120 | ||
|
|
fe3f21105c | ||
|
|
32264ac3e3 | ||
|
|
b34daeaacb | ||
|
|
d2c3a39ebb | ||
|
|
d10ac9b4a7 | ||
|
|
b21ed6a334 |
20
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
description: Report a reproducible bug in the current release of NetBox
|
||||
labels: ["type: bug"]
|
||||
labels: ["type: bug", "status: needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -10,16 +10,28 @@ body:
|
||||
installation. If you're having trouble with installation or just looking for
|
||||
assistance with using NetBox, please visit our
|
||||
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Deployment Type
|
||||
description: >
|
||||
How are you running NetBox? (For issues with the Docker image, please go to the
|
||||
[netbox-docker](https://github.com/netbox-community/netbox-docker) repo.)
|
||||
options:
|
||||
- NetBox Cloud
|
||||
- NetBox Enterprise
|
||||
- Self-hosted
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox version
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.6.6
|
||||
placeholder: v3.7.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Python version
|
||||
label: Python Version
|
||||
description: What version of Python are you currently running?
|
||||
options:
|
||||
- "3.8"
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -7,6 +7,9 @@ contact_links:
|
||||
- name: ❓ Discussion
|
||||
url: https://github.com/netbox-community/netbox/discussions
|
||||
about: "If you're just looking for help, try starting a discussion instead."
|
||||
- name: 🌎 Correct a Translation
|
||||
url: https://explore.transifex.com/netbox-community/netbox/
|
||||
about: "Spot an incorrect translation? You can propose a fix on Transifex."
|
||||
- name: 💡 Plugin Idea
|
||||
url: https://plugin-ideas.netbox.dev
|
||||
about: "Have an idea for a plugin? Head over to the ideas board!"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 📖 Documentation Change
|
||||
description: Suggest an addition or modification to the NetBox documentation
|
||||
labels: ["type: documentation"]
|
||||
labels: ["type: documentation", "status: needs triage"]
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: ✨ Feature Request
|
||||
description: Propose a new NetBox feature or enhancement
|
||||
labels: ["type: feature"]
|
||||
labels: ["type: feature", "status: needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.6.6
|
||||
placeholder: v3.7.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
21
.github/workflows/auto-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# auto-assign-issue (https://github.com/marketplace/actions/auto-assign-issue)
|
||||
name: Issue assignment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
auto-assign:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: pozil/auto-assign-issue@v1
|
||||
if: "contains(github.event.issue.labels.*.name, 'status: needs triage')"
|
||||
with:
|
||||
# Weighted assignments
|
||||
assignees: arthanson:3, jeffgdotorg:3, jeremystretch:3, abhi1693, DanSheps
|
||||
numOfAssignee: 1
|
||||
abortIfPreviousAssignees: true
|
||||
5
.github/workflows/ci.yml
vendored
@@ -68,6 +68,9 @@ jobs:
|
||||
- name: Collect static files
|
||||
run: python netbox/manage.py collectstatic --no-input
|
||||
|
||||
- name: Check for missing migrations
|
||||
run: python netbox/manage.py makemigrations --check
|
||||
|
||||
- name: Check PEP8 compliance
|
||||
run: pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/
|
||||
|
||||
@@ -81,4 +84,4 @@ jobs:
|
||||
run: coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel
|
||||
|
||||
- name: Show coverage report
|
||||
run: coverage report --skip-covered --omit *migrations*
|
||||
run: coverage report --skip-covered --omit '*/migrations/*,*/tests/*'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
|
||||
name: 'Close stale issues/PRs'
|
||||
name: Close stale issues/PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -12,10 +12,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
close-issue-message: >
|
||||
This issue has been automatically closed due to lack of activity. In an
|
||||
@@ -1,5 +1,5 @@
|
||||
# lock-threads (https://github.com/marketplace/actions/lock-threads)
|
||||
name: 'Lock threads'
|
||||
name: Lock threads
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -9,13 +9,15 @@ on:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
issue-inactive-days: 90
|
||||
pr-inactive-days: 30
|
||||
discussion-inactive-days: 180
|
||||
issue-lock-reason: 'resolved'
|
||||
@@ -36,6 +36,8 @@ NetBox users are welcome to participate in either role, on stage or in the crowd
|
||||
|
||||
## :bug: Reporting Bugs
|
||||
|
||||
:warning: Bug reports are used to call attention to some unintended or unexpected behavior in NetBox, such as when an error occurs or when the result of taking some action is inconsistent with the documentation. **Bug reports may not be used to suggest new functionality**; please see "feature requests" below if that is your goal.
|
||||
|
||||
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's likely that the bug has already been fixed.
|
||||
|
||||
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
|
||||
@@ -84,12 +86,16 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
|
||||
|
||||
* 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 resulting from multiple PRs being submitted simultaneously.)
|
||||
|
||||
* All code submissions should meet the following criteria (CI will enforce these checks):
|
||||
* All code submissions must meet the following criteria (CI will enforce these checks where feasible):
|
||||
* Consist entirely of original work
|
||||
* Python syntax is valid
|
||||
* All tests pass when run with `./manage.py test`
|
||||
* PEP 8 compliance is enforced, with the exception that lines may be
|
||||
greater than 80 characters in length
|
||||
|
||||
> [!CAUTION]
|
||||
> Any contributions which include AI-generated or reproduced content will be rejected.
|
||||
|
||||
* Some other tips to keep in mind:
|
||||
* If you'd like to volunteer for someone else's issue, please post a comment on that issue letting us know. (This will allow the maintainers to assign it to you.)
|
||||
* Check out our [developer docs](https://docs.netbox.dev/en/stable/development/getting-started/) for tips on setting up your development environment.
|
||||
@@ -115,8 +121,6 @@ We're always looking for motivated individuals to join the maintainers team and
|
||||
|
||||
We generally ask that maintainers dedicate around four hours of work to the project each week on average, which includes both hands-on development and project management tasks such as issue triage. Maintainers are also encouraged (but not required) to attend our bi-weekly Zoom call to catch up on recent items.
|
||||
|
||||
Many maintainers petition their employer to grant some of their paid time to work on NetBox. In doing so, your employer becomes eligible to be featured as a [NetBox sponsor](https://github.com/netbox-community/netbox/wiki/Sponsorship).
|
||||
|
||||
Interested? You can contact our lead maintainer, Jeremy Stretch, at jeremy@netbox.dev or on the [NetDev Community Slack](https://netdev.chat/). We'd love to have you on the team!
|
||||
|
||||
## :heart: Other Ways to Contribute
|
||||
|
||||
153
README.md
@@ -1,86 +1,129 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||
<p>The premier source of truth powering network automation</p>
|
||||
<img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" />
|
||||
<p><strong>The cornerstone of every automated network</strong></p>
|
||||
<a href="https://github.com/netbox-community/netbox/releases"><img src="https://img.shields.io/github/v/release/netbox-community/netbox" alt="Latest release" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-7-blue" alt="Languages supported" /></a>
|
||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
|
||||
<p></p>
|
||||
</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 serves
|
||||
as the cornerstone for network automation in thousands of organizations.
|
||||
NetBox exists to empower network engineers. Since its release in 2016, it has become the go-to solution for modeling and documenting network infrastructure for thousands of organizations worldwide. As a successor to legacy IPAM and DCIM applications, NetBox provides a cohesive, extensive, and accessible data model for all things networked. By providing a single robust user interface and programmable APIs for everything from cable maps to device configurations, NetBox serves as the central source of truth for the modern network.
|
||||
|
||||
* **Physical infrastructure:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power!
|
||||
* **Modern IPAM:** All the standard IPAM functionality you expect, plus VRF import/export tracking, VLAN management, and overlay support.
|
||||
* **Data circuits:** Confidently manage the delivery of critical circuits from various service providers, modeled seamlessly alongside your own infrastructure.
|
||||
* **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets.
|
||||
* **Organization:** Manage tenant and contact assignments natively.
|
||||
* **Powerful search:** Easily find anything you need using a single global search function.
|
||||
* **Comprehensive logging:** Leverage both automatic change logging and user-submitted journal entries to track your network's growth over time.
|
||||
* **Endless customization:** Custom fields, custom links, tags, export templates, custom validation, reports, scripts, and more!
|
||||
* **Flexible permissions:** An advanced permissions systems enables very flexible delegation of permissions.
|
||||
* **Integrations:** Easily connect NetBox to your other tooling via its REST & GraphQL APIs.
|
||||
* **Plugins:** Not finding what you need in the core application? Try one of many community plugins - or build your own!
|
||||
<p align="center">
|
||||
<a href="#netboxs-role">NetBox's Role</a> |
|
||||
<a href="#why-netbox">Why NetBox?</a> |
|
||||
<a href="#getting-started">Getting Started</a> |
|
||||
<a href="#get-involved">Get Involved</a> |
|
||||
<a href="#project-stats">Project Stats</a> |
|
||||
<a href="#screenshots">Screenshots</a>
|
||||
</p>
|
||||
|
||||

|
||||
<p align="center">
|
||||
<img src="docs/media/screenshots/home-light.png" width="600" alt="NetBox user interface screenshot" />
|
||||
</p>
|
||||
|
||||
## NetBox's Role
|
||||
|
||||
NetBox functions as the **source of truth** for your network infrastructure. Its job is to define and validate the _intended state_ of all network components and resources. NetBox does not interact with network nodes directly; rather, it makes this data available programmatically to purpose-built automation, monitoring, and assurance tools. This separation of duties enables the construction of a robust yet flexible automation system.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/misc/reference_architecture.png" alt="Reference network automation architecture" />
|
||||
</p>
|
||||
|
||||
The diagram above illustrates the recommended deployment architecture for an automated network, leveraging NetBox as the central authority for network state. This approach allows your team to swap out individual tools to meet changing needs while retaining a predictable, modular workflow.
|
||||
|
||||
## Why NetBox?
|
||||
|
||||
### Comprehensive Data Model
|
||||
|
||||
Racks, devices, cables, IP addresses, VLANs, circuits, power, VPNs, and lots more: NetBox is built for networks. Its comprehensive and thoroughly inter-linked data model provides for natural and highly structured modeling of myriad network primitives that just isn't possible using general-purpose tools. And there's no need to waste time contemplating how to build out a database: Everything is ready to go upon installation.
|
||||
|
||||
### Focused Development
|
||||
|
||||
NetBox strives to meet a singular goal: Provide the best available solution for making network infrastructure programmatically accessible. Unlike "all-in-one" tools which awkwardly bolt on half-baked features in an attempt to check every box, NetBox is committed to its core function. NetBox provides the best possible solution for modeling network infrastructure, and provides rich APIs for integrating with tools that excel in other areas of network automation.
|
||||
|
||||
### Extensible and Customizable
|
||||
|
||||
No two networks are exactly the same. Users are empowered to extend NetBox's native data model with custom fields and tags to best suit their unique needs. You can even write your own plugins to introduce entirely new objects and functionality!
|
||||
|
||||
### Flexible Permissions
|
||||
|
||||
NetBox includes a fully customizable permission system, which affords administrators incredible granularity when assigning roles to users and groups. Want to restrict certain users to working only with cabling and not be able to change IP addresses? Or maybe each team should have access only to a particular tenant? NetBox enables you to craft roles as you see fit.
|
||||
|
||||
### Custom Validation & Protection Rules
|
||||
|
||||
The data you put into NetBox is crucial to network operations. In addition to its robust native validation rules, NetBox provides mechanisms for administrators to define their own custom validation rules for objects. Custom validation can be used both to ensure new or modified objects adhere to a set of rules, and to prevent the deletion of objects which don't meet certain criteria. (For example, you might want to prevent the deletion of a device with an "active" status.)
|
||||
|
||||
### Device Configuration Rendering
|
||||
|
||||
NetBox can render user-created Jinja2 templates to generate device configurations from its own data. Configuration templates can be uploaded individually or pulled automatically from an external source, such as a git repository. Rendered configurations can be retrieved via the REST API for application directly to network devices via a provisioning tool such as Ansible or Salt.
|
||||
|
||||
### Custom Scripts
|
||||
|
||||
Complex workflows, such as provisioning a new branch office, can be tedious to carry out via the user interface. NetBox allows you to write and upload custom scripts that can be run directly from the UI. Scripts prompt users for input and then automate the necessary tasks to greatly simplify otherwise burdensome processes.
|
||||
|
||||
### Automated Events
|
||||
|
||||
Users can define event rules to automatically trigger a custom script or outbound webhook in response to a NetBox event. For example, you might want to automatically update a network monitoring service whenever a new device is added to NetBox, or update a DHCP server when an IP range is allocated.
|
||||
|
||||
### Comprehensive Change Logging
|
||||
|
||||
NetBox automatically logs the creation, modification, and deletion of all managed objects, providing a thorough change history. Changes can be attributed to the executing user, and related changes are grouped automatically by request ID.
|
||||
|
||||
> [!NOTE]
|
||||
> A complete list of NetBox's myriad features can be found in [the introductory documentation](https://docs.netbox.dev/en/stable/introduction/).
|
||||
|
||||
## Getting Started
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/netbox-community/netbox)
|
||||
|
||||
[](https://github.com/netbox-community/netbox-docker)
|
||||
|
||||
[](https://netboxlabs.com/netbox-cloud/)
|
||||
|
||||
</div>
|
||||
|
||||
* Just want to explore? Check out [our public demo](https://demo.netbox.dev/) right now!
|
||||
* The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction.
|
||||
* Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox!
|
||||
|
||||
<p align="center">
|
||||
<a href="https://netboxlabs.com/netbox-cloud/"><img src="docs/media/misc/netbox_cloud.png" alt="NetBox Cloud" /></a><br />
|
||||
Looking for a managed solution? Check out <strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> or <strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>!
|
||||
</p>
|
||||
|
||||
## Get Involved
|
||||
|
||||
* Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter!
|
||||
* Join the conversation on [the discussion forum](https://github.com/netbox-community/netbox/discussions) and [Slack](https://netdev.chat/)!
|
||||
* Already a power user? You can [suggest a feature](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+feature&template=feature_request.yaml) or [report a bug](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+bug&template=bug_report.yaml) on GitHub.
|
||||
* Contributions from the community are encouraged and appreciated! Check out our [contributing guide](CONTRIBUTING.md) to get started.
|
||||
* [Share your idea](https://plugin-ideas.netbox.dev/) for a new plugin, or [learn how to build one](https://github.com/netbox-community/netbox-plugin-tutorial) yourself!
|
||||
|
||||
## Project Stats
|
||||
|
||||
<div align="center">
|
||||
<p align="center">
|
||||
<a href="https://github.com/netbox-community/netbox/commits"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/whQtEr_TGD9PhW1BPlhlEQ5jnrgQ0KJpm-LlGtpoGO0/3Kx_iWUSBRJ5-AI4QwJEJWrUDEz3KrX2lvh8aYE0WXY_timeline.svg" alt="Timeline graph"></a>
|
||||
<a href="https://github.com/netbox-community/netbox/issues"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/whQtEr_TGD9PhW1BPlhlEQ5jnrgQ0KJpm-LlGtpoGO0/3Kx_iWUSBRJ5-AI4QwJEJWrUDEz3KrX2lvh8aYE0WXY_issues.svg" alt="Issues graph"></a>
|
||||
<a href="https://github.com/netbox-community/netbox/pulls"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/whQtEr_TGD9PhW1BPlhlEQ5jnrgQ0KJpm-LlGtpoGO0/3Kx_iWUSBRJ5-AI4QwJEJWrUDEz3KrX2lvh8aYE0WXY_prs.svg" alt="Pull requests graph"></a>
|
||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/whQtEr_TGD9PhW1BPlhlEQ5jnrgQ0KJpm-LlGtpoGO0/3Kx_iWUSBRJ5-AI4QwJEJWrUDEz3KrX2lvh8aYE0WXY_users.svg" alt="Top contributors"></a>
|
||||
<br />Stats via <a href="https://repography.com">Repography</a>
|
||||
</div>
|
||||
|
||||
## Sponsors
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://netboxlabs.com)
|
||||
|
||||
[](https://try.digitalocean.com/developer-cloud)
|
||||
|
||||
[](https://sentry.io)
|
||||
<br />
|
||||
[](https://metal.equinix.com)
|
||||
|
||||
[](https://onemindservices.com)
|
||||
|
||||
</div>
|
||||
</p>
|
||||
|
||||
## Screenshots
|
||||
|
||||
")
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
<p align="center">
|
||||
<strong>NetBox Dashboard (Light Mode)</strong><br />
|
||||
<img src="docs/media/screenshots/home-light.png" width="600" alt="NetBox dashboard (light mode)" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<strong>NetBox Dashboard (Dark Mode)</strong><br />
|
||||
<img src="docs/media/screenshots/home-dark.png" width="600" alt="NetBox dashboard (dark mode)" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<strong>Prefixes List</strong><br />
|
||||
<img src="docs/media/screenshots/prefixes-list.png" width="600" alt="Prefixes list" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<strong>Rack View</strong><br />
|
||||
<img src="docs/media/screenshots/rack.png" width="600" alt="Rack view" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<strong>Cable Trace</strong><br />
|
||||
<img src="docs/media/screenshots/cable-trace.png" width="600" alt="Cable trace" />
|
||||
</p>
|
||||
|
||||
@@ -61,7 +61,8 @@ django-timezone-field
|
||||
|
||||
# A REST API framework for Django projects
|
||||
# https://www.django-rest-framework.org/community/release-notes/
|
||||
djangorestframework
|
||||
# Pinned to 3.14 for NetBox v3.7
|
||||
djangorestframework<3.15
|
||||
|
||||
# Sane and flexible OpenAPI 3 schema generation for Django REST framework.
|
||||
# https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst
|
||||
@@ -101,11 +102,11 @@ markdown-include
|
||||
mkdocs-material
|
||||
|
||||
# Introspection for embedded code
|
||||
# https://github.com/mkdocstrings/mkdocstrings/blob/master/CHANGELOG.md
|
||||
# https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md
|
||||
mkdocstrings[python-legacy]
|
||||
|
||||
# Library for manipulating IP prefixes and addresses
|
||||
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG
|
||||
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst
|
||||
netaddr
|
||||
|
||||
# Fork of PIL (Python Imaging Library) for image processing
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"type": "object",
|
||||
"$id": "urn:devicetype-library:generated-schema",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"airflow": {
|
||||
@@ -384,7 +386,10 @@
|
||||
"8gfc-sfpp",
|
||||
"16gfc-sfpp",
|
||||
"32gfc-sfp28",
|
||||
"32gfc-sfpp",
|
||||
"64gfc-qsfpp",
|
||||
"64gfc-sfpdd",
|
||||
"64gfc-sfpp",
|
||||
"128gfc-qsfp28",
|
||||
"infiniband-sdr",
|
||||
"infiniband-ddr",
|
||||
|
||||
4
docs/_theme/main.html
vendored
@@ -2,8 +2,8 @@
|
||||
|
||||
{% block site_meta %}
|
||||
{{ super() }}
|
||||
{# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
|
||||
{% if page.canonical_url != 'https://docs.netbox.dev/' %}
|
||||
{# Disable search indexing unless we're building for ReadTheDocs #}
|
||||
{% if not config.extra.readthedocs %}
|
||||
<meta name="robots" content="noindex">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -73,7 +73,7 @@ You should be redirected to Microsoft's authentication portal. Enter the usernam
|
||||
|
||||
If successful, you will be redirected back to the NetBox UI, and will be logged in as the AD user. You can verify this by navigating to your profile (using the button at top right).
|
||||
|
||||
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions within the NetBox admin UI.
|
||||
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions by navigating to Admin > Permissions.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -67,4 +67,4 @@ You should be redirected to Okta's authentication portal. Enter the username/ema
|
||||
|
||||
If successful, you will be redirected back to the NetBox UI, and will be logged in as the Okta user. You can verify this by navigating to your profile (using the button at top right).
|
||||
|
||||
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions within the NetBox admin UI.
|
||||
This user account has been replicated locally to NetBox, and can now be assigned groups and permissions by navigating to Admin > Permissions.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Local Authentication
|
||||
|
||||
Local user accounts and groups can be created in NetBox under the "Authentication and Authorization" section of the administrative user interface. This interface is available only to users with the "staff" permission enabled.
|
||||
Local user accounts and groups can be created in NetBox under the "Authentication" section in the "Admin" menu. This section is available only to users with the "staff" permission enabled.
|
||||
|
||||
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to users and/or groups within the admin UI.
|
||||
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](../permissions.md) may also be assigned to users and/or groups under Admin > Permissions.
|
||||
|
||||
## Remote Authentication
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
|
||||
|
||||
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date). Default formats are listed below.
|
||||
|
||||
!!! note
|
||||
These system defaults will be overridden by a user's selected language/locale when [localization](./system.md#enable_localization) is enabled.
|
||||
|
||||
```python
|
||||
DATE_FORMAT = 'N j, Y' # June 26, 2016
|
||||
SHORT_DATE_FORMAT = 'Y-m-d' # 2016-06-26
|
||||
|
||||
@@ -46,4 +46,4 @@ The configuration file may be modified at any time. However, the WSGI service (e
|
||||
$ sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
Configuration parameters which are set via the admin UI (those listed under "dynamic settings") take effect immediately.
|
||||
Dynamic configuration parameters (those which can be modified via the UI) take effect immediately.
|
||||
|
||||
@@ -80,6 +80,17 @@ changes in the database indefinitely.
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG_SKIP_EMPTY_CHANGES
|
||||
|
||||
Default: True
|
||||
|
||||
If enabled, a change log record will not be created when an object is updated without any changes to its existing field values.
|
||||
|
||||
!!! note
|
||||
The object's `last_updated` field will always reflect the time of the most recent update, regardless of this parameter.
|
||||
|
||||
---
|
||||
|
||||
## DATA_UPLOAD_MAX_MEMORY_SIZE
|
||||
|
||||
Default: `2621440` (2.5 MB)
|
||||
@@ -92,9 +103,12 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
|
||||
|
||||
!!! tip "Dynamic Configuration Parameter"
|
||||
|
||||
Default: False
|
||||
Default: True
|
||||
|
||||
By default, NetBox will permit users to create duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This behavior can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to True.
|
||||
By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to False.
|
||||
|
||||
!!! info "Changed in v3.7"
|
||||
The default value for this parameter was changed from False to True in NetBox v3.7.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ When remote user authentication is in use, this is the name of the HTTP header w
|
||||
|
||||
Default: `|` (Pipe)
|
||||
|
||||
The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
The Separator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -183,6 +183,30 @@ The view name or URL to which a user is redirected after logging out.
|
||||
|
||||
---
|
||||
|
||||
## SECURE_HSTS_INCLUDE_SUBDOMAINS
|
||||
|
||||
Default: False
|
||||
|
||||
If true, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
|
||||
|
||||
---
|
||||
|
||||
## SECURE_HSTS_PRELOAD
|
||||
|
||||
Default: False
|
||||
|
||||
If true, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
|
||||
|
||||
---
|
||||
|
||||
## SECURE_HSTS_SECONDS
|
||||
|
||||
Default: 0
|
||||
|
||||
If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict Transport Security (HSTS) header on all responses that do not already have it. This will instruct the browser that the website must be accessed via HTTPS, blocking any HTTP request.
|
||||
|
||||
---
|
||||
|
||||
## SECURE_SSL_REDIRECT
|
||||
|
||||
Default: False
|
||||
|
||||
@@ -16,10 +16,7 @@ BASE_PATH = 'netbox/'
|
||||
|
||||
Default: `en-us` (US English)
|
||||
|
||||
Defines the default preferred language/locale for requests that do not specify one. This is used to alter e.g. the display of dates and numbers to fit the user's locale. See [this list](http://www.i18nguy.com/unicode/language-identifiers.html) of standard language codes. (This parameter maps to Django's [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#language-code) internal setting.)
|
||||
|
||||
!!! note
|
||||
Altering this parameter will *not* change the language used in NetBox. We hope to provide translation support in a future NetBox release.
|
||||
Defines the default preferred language/locale for requests that do not specify one. (This parameter maps to Django's [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#language-code) internal setting.)
|
||||
|
||||
---
|
||||
|
||||
@@ -69,15 +66,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
|
||||
|
||||
Default: False
|
||||
|
||||
Determines if localization features are enabled or not. This should only be enabled for development or testing purposes as netbox is not yet fully localized. Turning this on will localize numeric and date formats (overriding what is set for DATE_FORMAT) based on the browser locale as well as translate certain strings from third party modules.
|
||||
|
||||
---
|
||||
|
||||
## GIT_PATH
|
||||
|
||||
Default: `git`
|
||||
|
||||
The system path to the `git` executable, used by the synchronization backend for remote git repositories.
|
||||
Determines if localization features are enabled or not. This should only be enabled for development or testing purposes as netbox is not yet fully localized. Turning this on will localize numeric and date formats (overriding any configured [system defaults](./date-time.md#date-and-time-formatting)) based on the browser locale as well as translate certain strings from third party modules.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -288,9 +288,9 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
|
||||
## Running Custom Scripts
|
||||
|
||||
!!! note
|
||||
To run a custom script, a user must be assigned via permissions for `Extras > Script`, `Extras > ScriptModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.
|
||||
To run a custom script, a user must be assigned permissions for `Extras > Script`, `Extras > Script Module`, and `Core > Managed File` objects. They must also be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in "Permissions" as shown below.
|
||||
|
||||

|
||||

|
||||
|
||||
### Via the Web UI
|
||||
|
||||
@@ -390,7 +390,7 @@ class NewBranchScript(Script):
|
||||
name=f'{site.slug}-switch{i}',
|
||||
site=site,
|
||||
status=DeviceStatusChoices.STATUS_PLANNED,
|
||||
role=switch_role
|
||||
device_role=switch_role
|
||||
)
|
||||
switch.full_clean()
|
||||
switch.save()
|
||||
|
||||
@@ -132,9 +132,9 @@ Once you have created a report, it will appear in the reports list. Initially, r
|
||||
## Running Reports
|
||||
|
||||
!!! note
|
||||
To run a report, a user must be assigned via permissions for `Extras > Report`, `Extras > ReportModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.
|
||||
To run a report, a user must be assigned permissions for `Extras > Report`, `Extras > Report Module`, and `Core > Managed File` objects. They must also be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in "Permissions" as shown below.
|
||||
|
||||

|
||||

|
||||
|
||||
### Via the Web UI
|
||||
|
||||
|
||||
@@ -2,12 +2,25 @@
|
||||
|
||||
Below is a list of tasks to consider when adding a new field to a core model.
|
||||
|
||||
## 1. Generate and run database migrations
|
||||
## 1. Add the field to the model class
|
||||
|
||||
Add the field to the model, taking care to address any of the following conditions.
|
||||
|
||||
* When adding a GenericForeignKey field, also add an index under `Meta` for its two concrete fields. For example:
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
indexes = (
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
```
|
||||
|
||||
## 2. Generate and run database migrations
|
||||
|
||||
[Django migrations](https://docs.djangoproject.com/en/stable/topics/migrations/) are used to express changes to the database schema. In most cases, Django can generate these automatically, however very complex changes may require manual intervention. Always remember to specify a short but descriptive name when generating a new migration.
|
||||
|
||||
```
|
||||
./manage.py makemigrations <app> -n <name>
|
||||
./manage.py makemigrations <app> -n <name> --no-header
|
||||
./manage.py migrate
|
||||
```
|
||||
|
||||
@@ -16,7 +29,7 @@ Where possible, try to merge related changes into a single migration. For exampl
|
||||
!!! warning "Do not alter existing migrations"
|
||||
Migrations can only be merged within a release. Once a new release has been published, its migrations cannot be altered (other than for the purpose of correcting a bug).
|
||||
|
||||
## 2. Add validation logic to `clean()`
|
||||
## 3. Add validation logic to `clean()`
|
||||
|
||||
If the new field introduces additional validation requirements (beyond what's included with the field itself), implement them in the model's `clean()` method. Remember to call the model's original method using `super()` before or after your custom validation as appropriate:
|
||||
|
||||
@@ -31,15 +44,15 @@ class Foo(models.Model):
|
||||
raise ValidationError()
|
||||
```
|
||||
|
||||
## 3. Update relevant querysets
|
||||
## 4. Update relevant querysets
|
||||
|
||||
If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retrieving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid extraneous database queries.
|
||||
|
||||
## 4. Update API serializer
|
||||
## 5. Update API serializer
|
||||
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal representation of the model.
|
||||
|
||||
## 5. Add fields to forms
|
||||
## 6. Add fields to forms
|
||||
|
||||
Extend any forms to include the new field(s) as appropriate. These are found under the `forms/` directory within each app. Common forms include:
|
||||
|
||||
@@ -48,23 +61,23 @@ Extend any forms to include the new field(s) as appropriate. These are found und
|
||||
* **CSV import** - The form used when bulk importing objects in CSV format
|
||||
* **Filter** - Displays the options available for filtering a list of objects (both UI and API)
|
||||
|
||||
## 6. Extend object filter set
|
||||
## 7. Extend object filter set
|
||||
|
||||
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to query it in the FilterSet's `search()` method.
|
||||
|
||||
## 7. Add column to object table
|
||||
## 8. Add column to object table
|
||||
|
||||
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require declaring a custom column. Also add the field name to `default_columns` if the column should be present in the table by default.
|
||||
|
||||
## 8. Update the SearchIndex
|
||||
## 9. Update the SearchIndex
|
||||
|
||||
Where applicable, add the new field to the model's SearchIndex for inclusion in global search.
|
||||
|
||||
## 9. Update the UI templates
|
||||
## 10. Update the UI templates
|
||||
|
||||
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
|
||||
|
||||
## 10. Create/extend test cases
|
||||
## 11. Create/extend test cases
|
||||
|
||||
Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including:
|
||||
|
||||
@@ -74,8 +87,8 @@ Create or extend the relevant test cases to verify that the new field and any ac
|
||||
* Model tests
|
||||
* View tests
|
||||
|
||||
Be diligent to ensure all of the relevant test suites are adapted or extended as necessary to test any new functionality.
|
||||
Be diligent to ensure all the relevant test suites are adapted or extended as necessary to test any new functionality.
|
||||
|
||||
## 11. Update the model's documentation
|
||||
## 12. Update the model's documentation
|
||||
|
||||
Each model has a dedicated page in the documentation, at `models/<app>/<model>.md`. Update this file to include any relevant information about the new field.
|
||||
|
||||
@@ -80,6 +80,18 @@ Run the following command to update the device type definition validation schema
|
||||
|
||||
This will automatically update the schema file at `contrib/generated_schema.json`.
|
||||
|
||||
### Update & Compile Translations
|
||||
|
||||
Log into [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) to download the updated string maps. Download the resource (portable object, or `.po`) file for each language and save them to `netbox/translations/$lang/LC_MESSAGES/django.po`, overwriting the current files. (Be sure to click the **Download for use** link.)
|
||||
|
||||

|
||||
|
||||
Once the resource files for all languages have been updated, compile the machine object (`.mo`) files using the `compilemessages` management command:
|
||||
|
||||
```nohighlight
|
||||
./manage.py compilemessages
|
||||
```
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
* Update the `VERSION` constant in `settings.py` to the new release version.
|
||||
@@ -90,7 +102,7 @@ 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.
|
||||
Ensure that continuous integration testing on the `develop` branch is completing successfully. If it fails, take action to correct the failure before proceeding with the release.
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
|
||||
30
docs/development/translations.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Translations
|
||||
|
||||
NetBox coordinates all translation work using the [Transifex](https://explore.transifex.com/netbox-community/netbox/) platform. Signing up for a Transifex account is free.
|
||||
|
||||
All language translations in NetBox are generated from the source file found at `netbox/translations/en/LC_MESSAGES/django.po`. This file contains the original English strings with empty mappings, and is generated as part of NetBox's release process. Transifex updates source strings from this file on a recurring basis, so new translation strings will appear in the platform automatically as it is updated in the code base.
|
||||
|
||||
Reviewers log into Transifex and navigate to their designated language(s) to translate strings. The initial translation for most strings will be machine-generated via the AWS Translate service. Human reviewers are responsible for reviewing these translations and making corrections where necessary.
|
||||
|
||||
Immediately prior to each NetBox release, the translation maps for all completed languages will be downloaded from Transifex, compiled, and checked into the NetBox code base by a maintainer.
|
||||
|
||||
## Updating Translation Sources
|
||||
|
||||
To update the English `.po` file from which all translations are derived, use the `makemessages` management command:
|
||||
|
||||
```nohighlight
|
||||
./manage.py makemessages -l en
|
||||
```
|
||||
|
||||
Then, commit the change and push to the `develop` branch on GitHub. After some time, any new strings will appear for translation on Transifex automatically.
|
||||
|
||||
## Proposing New Languages
|
||||
|
||||
If you'd like to add support for a new language to NetBox, the first step is to [submit a GitHub issue](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+translation&projects=&template=translation.yaml) to capture the proposal. While we'd like to add as many languages as possible, we do need to limit the rate at which new languages are added. New languages will be selected according to community interest and the number of volunteers who sign up as translators.
|
||||
|
||||
Once a proposed language has been approved, a NetBox maintainer will:
|
||||
|
||||
* Add it to the Transifex platform
|
||||
* Designate one or more reviewers
|
||||
* Create the initial machine-generated translations for review
|
||||
* Add it to the list of supported languages
|
||||
@@ -39,7 +39,7 @@ When rendered for a specific NetBox device, the template's `device` variable wil
|
||||
|
||||
### Context Data
|
||||
|
||||
The objet for which the configuration is being rendered is made available as template context as `device` or `virtualmachine` for devices and virtual machines, respectively. Additionally, NetBox model classes can be accessed by the app or plugin in which they reside. For example:
|
||||
The object for which the configuration is being rendered is made available as template context as `device` or `virtualmachine` for devices and virtual machines, respectively. Additionally, NetBox model classes can be accessed by the app or plugin in which they reside. For example:
|
||||
|
||||
```
|
||||
There are {{ dcim.Site.objects.count() }} sites.
|
||||
@@ -70,6 +70,11 @@ This request will trigger resolution of the device's preferred config template i
|
||||
|
||||
If no config template has been assigned to any of these three objects, the request will fail.
|
||||
|
||||
The configuration can be rendered as JSON or as plaintext by setting the `Accept:` HTTP header. For example:
|
||||
|
||||
* `Accept: application/json`
|
||||
* `Accept: text/plain`
|
||||
|
||||
### General Purpose Use
|
||||
|
||||
NetBox config templates can also be rendered without being tied to any specific device, using a separate general purpose REST API endpoint. Any data included with a POST request to this endpoint will be passed as context data for the template.
|
||||
|
||||
@@ -8,6 +8,9 @@ When entering a search query, the user can choose a specific lookup type: exact
|
||||
|
||||
Custom fields defined by NetBox administrators are also included in search results if configured with a search weight. Additionally, NetBox plugins can register their own custom models for inclusion alongside core models.
|
||||
|
||||
!!! note
|
||||
NetBox does not index any static choice field's (including custom fields of type "Selection" or "Multiple selection").
|
||||
|
||||
## Saved Filters
|
||||
|
||||
Each type of object in NetBox is accompanied by an extensive set of filters, each tied to a specific attribute, which enable the creation of complex queries. Often you'll find that certain queries are used routinely to apply some set of prescribed conditions to a query. Once a set of filters has been applied, NetBox offers the option to save it for future use.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Synchronized Data
|
||||
|
||||
Several models in NetBox support the automatic synchronization of local data from a designated remote source. For example, [configuration templates](./configuration-rendering.md) defined in NetBox can source their content from text files stored in a remote git repository. This accomplished using the core [data source](../models/core/datasource.md) and [data file](../models/core/datafile.md) models.
|
||||
Several models in NetBox support the automatic synchronization of local data from a designated remote source. For example, [configuration templates](./configuration-rendering.md) defined in NetBox can source their content from text files stored in a remote git repository. This is accomplished using the core [data source](../models/core/datasource.md) and [data file](../models/core/datafile.md) models.
|
||||
|
||||
To enable remote data synchronization, the NetBox administrator first designates one or more remote data sources. NetBox currently supports the following source types:
|
||||
|
||||
@@ -10,7 +10,6 @@ To enable remote data synchronization, the NetBox administrator first designates
|
||||
|
||||
(Local disk paths are considered "remote" in this context as they exist outside NetBox's database. These paths could also be mapped to external network shares.)
|
||||
|
||||
|
||||
!!! info
|
||||
Data backends which connect to external sources typically require the installation of one or more supporting Python libraries. The Git backend requires the [`dulwich`](https://www.dulwich.io/) package, and the S3 backend requires the [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) package. These must be installed within NetBox's environment to enable these backends.
|
||||
|
||||
@@ -23,3 +22,6 @@ The following NetBox models can be associated with replicated data files:
|
||||
* Export templates
|
||||
|
||||
Once a data has been designated for a local instance, its data will be replaced with the content of the replicated file. When the replicated file is updated in the future (via synchronization jobs), the local instance will be flagged as having out-of-date data. A user can then synchronize these objects individually or in bulk to effect the update. This two-stage process ensures that automated synchronization tasks do not immediately affect production data.
|
||||
|
||||
!!! note "Permissions"
|
||||
A user must be assigned the `core.sync_datasource` permission in order to synchronize local files from a remote data source.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
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)
|
||||
[](./media/screenshots/home-light.png)
|
||||
|
||||
## :material-server-network: Built for Networks
|
||||
|
||||
|
||||
@@ -31,8 +31,7 @@ This section entails the installation and configuration of a local PostgreSQL da
|
||||
Once PostgreSQL has been installed, start the service and enable it to run at boot:
|
||||
|
||||
```no-highlight
|
||||
sudo systemctl start postgresql
|
||||
sudo systemctl enable postgresql
|
||||
sudo systemctl enable --now postgresql
|
||||
```
|
||||
|
||||
Before continuing, verify that you have installed PostgreSQL 12 or later:
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
|
||||
```no-highlight
|
||||
sudo yum install -y redis
|
||||
sudo systemctl start redis
|
||||
sudo systemctl enable redis
|
||||
sudo systemctl enable --now redis
|
||||
```
|
||||
|
||||
Before continuing, verify that your installed version of Redis is at least v4.0:
|
||||
|
||||
@@ -27,8 +27,7 @@ sudo systemctl daemon-reload
|
||||
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
|
||||
|
||||
```no-highlight
|
||||
sudo systemctl start netbox netbox-rq
|
||||
sudo systemctl enable netbox netbox-rq
|
||||
sudo systemctl enable --now netbox netbox-rq
|
||||
```
|
||||
|
||||
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
|
||||
@@ -58,3 +57,6 @@ You should see output similar to the following:
|
||||
If the NetBox service fails to start, issue the command `journalctl -eu netbox` to check for log messages that may indicate the problem.
|
||||
|
||||
Once you've verified that the WSGI workers are up and running, move on to HTTP server setup.
|
||||
|
||||
!!! note
|
||||
There is a bug in the current stable release of gunicorn (v21.2.0) where automatic restarts of the worker processes can result in 502 errors under heavy load. (See [gunicorn bug #3038](https://github.com/benoitc/gunicorn/issues/3038) for more detail.) Users who encounter this issue may opt to downgrade to an earlier, unaffected release of gunicorn (`pip install gunicorn==20.1.0`). Note, however, that this earlier release does not officially support Python 3.11.
|
||||
|
||||
@@ -85,13 +85,19 @@ Each model generally has two views associated with it: a list view and a detail
|
||||
* `/api/dcim/devices/` - List existing devices or create a new device
|
||||
* `/api/dcim/devices/123/` - Retrieve, update, or delete the device with ID 123
|
||||
|
||||
Lists of objects can be filtered using a set of query parameters. For example, to find all interfaces belonging to the device with ID 123:
|
||||
Lists of objects can be filtered and ordered using a set of query parameters. For example, to find all interfaces belonging to the device with ID 123:
|
||||
|
||||
```
|
||||
GET /api/dcim/interfaces/?device_id=123
|
||||
```
|
||||
|
||||
See the [filtering documentation](../reference/filtering.md) for more details.
|
||||
An optional `ordering` parameter can be used to define how to sort the results. Building off the previous example, to sort all the interfaces in reverse order of creation (newest to oldest) for a device with ID 123:
|
||||
|
||||
```
|
||||
GET /api/dcim/interfaces/?device_id=123&ordering=-created
|
||||
```
|
||||
|
||||
See the [filtering documentation](../reference/filtering.md) for more details on topics related to filtering, ordering and lookup expressions.
|
||||
|
||||
## Serialization
|
||||
|
||||
@@ -647,18 +653,20 @@ Note that we are _not_ passing an existing REST API token with this request. If
|
||||
{
|
||||
"id": 6,
|
||||
"url": "https://netbox/api/users/tokens/6/",
|
||||
"display": "3c9cb9 (hankhill)",
|
||||
"display": "**********************************3c9cb9",
|
||||
"user": {
|
||||
"id": 2,
|
||||
"url": "https://netbox/api/users/users/2/",
|
||||
"display": "hankhill",
|
||||
"username": "hankhill"
|
||||
},
|
||||
"created": "2021-06-11T20:09:13.339367Z",
|
||||
"created": "2024-03-11T20:09:13.339367Z",
|
||||
"expires": null,
|
||||
"last_used": null,
|
||||
"key": "9fc9b897abec9ada2da6aec9dbc34596293c9cb9",
|
||||
"write_enabled": true,
|
||||
"description": ""
|
||||
"description": "",
|
||||
"allowed_ips": []
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
Some NetBox models support automatic synchronization of certain attributes from remote [data sources](../models/core/datasource.md), such as a git repository hosted on GitHub or GitLab. Data from the authoritative remote source is synchronized locally in NetBox as [data files](../models/core/datafile.md).
|
||||
|
||||
!!! note "Permissions"
|
||||
A user must be assigned the `core.sync_datasource` permission in order to synchronize local files from a remote data source. This is accomplished by creating a permission for the "Core > Data Source" object type with the `sync` action, and assigning it to the desired user and/or group.
|
||||
|
||||
The following features support the use of synchronized data:
|
||||
|
||||
* [Configuration templates](../features/configuration-rendering.md)
|
||||
|
||||
@@ -106,6 +106,6 @@ Content-Type: application/x-www-form-urlencoded
|
||||
------------
|
||||
```
|
||||
|
||||
Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection.
|
||||
Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection. If you don't see any output, check that the `rqworker` process is running and that webhook events are being placed into the queue.
|
||||
|
||||
Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI).
|
||||
Webhook results can be found in the NetBox admin UI under the Background Tasks section. You can see any finished or failed runs, as well as the error log for failed webhooks.
|
||||
|
||||
BIN
docs/media/development/transifex_download.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/media/misc/netbox_cloud.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
docs/media/misc/reference_architecture.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 316 KiB |
BIN
docs/media/screenshots/home-light.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 235 KiB |
@@ -19,7 +19,7 @@ The parent inventory item to which this item is assigned (optional).
|
||||
|
||||
### Name
|
||||
|
||||
The inventory item's name. Must be unique to the parent device.
|
||||
The inventory item's name. If the inventory item is assigned to a parent item, its name must be unique among its siblings (all items belonging to the same parent item).
|
||||
|
||||
### Label
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ The IKE version employed (v1 or v2).
|
||||
|
||||
### Mode
|
||||
|
||||
The IKE mode employed (main or aggressive).
|
||||
The mode employed (main or aggressive) when IKEv1 is in use. This setting is not supported for IKEv2.
|
||||
|
||||
### Proposals
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ The protocol employed for data encryption. Options include DES, 3DES, and variou
|
||||
|
||||
### Authentication Algorithm
|
||||
|
||||
The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations.
|
||||
The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations. Specifying an authentication algorithm is optional, as some encryption algorithms (e.g. AES-GCM) provide authentication natively.
|
||||
|
||||
### Group
|
||||
|
||||
|
||||
@@ -12,10 +12,16 @@ The unique user-assigned name for the proposal.
|
||||
|
||||
The protocol employed for data encryption. Options include DES, 3DES, and various flavors of AES.
|
||||
|
||||
!!! note
|
||||
If an encryption algorithm is not specified, an authentication algorithm must be specified.
|
||||
|
||||
### Authentication Algorithm
|
||||
|
||||
The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations.
|
||||
|
||||
!!! note
|
||||
If an authentication algorithm is not specified, an encryption algorithm must be specified.
|
||||
|
||||
### SA Lifetime (Seconds)
|
||||
|
||||
The maximum amount of time for which the security association (SA) may be active, in seconds.
|
||||
|
||||
@@ -47,3 +47,14 @@ class ReminderWidget(DashboardWidget):
|
||||
def render(self, request):
|
||||
return self.config.get('content')
|
||||
```
|
||||
|
||||
## Initialization
|
||||
|
||||
To register the widget, it becomes essential to import the widget module. The recommended approach is to accomplish this within the `ready` method situated in your `PluginConfig`:
|
||||
|
||||
```python
|
||||
class FooBarConfig(PluginConfig):
|
||||
def ready(self):
|
||||
super().ready()
|
||||
from . import widgets # point this to the above widget module you created
|
||||
```
|
||||
|
||||
@@ -20,4 +20,4 @@ backends = [MyDataBackend]
|
||||
!!! tip
|
||||
The path to the list of search indexes can be modified by setting `data_backends` in the PluginConfig instance.
|
||||
|
||||
::: core.data_backends.DataBackend
|
||||
::: netbox.data_backends.DataBackend
|
||||
|
||||
@@ -62,7 +62,7 @@ class MyModelImportForm(NetBoxModelImportForm):
|
||||
site = CSVModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned site'
|
||||
help_text=_('Assigned site')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
!!! tip "Plugins Development Tutorial"
|
||||
Just getting started with plugins? Check out our [**NetBox Plugin Tutorial**](https://github.com/netbox-community/netbox-plugin-tutorial) on GitHub! This in-depth guide will walk you through the process of creating an entire plugin from scratch. It even includes a companion [demo plugin repo](https://github.com/netbox-community/netbox-plugin-demo) to ensure you can jump in at any step along the way. This will get you up and running with plugins in no time!
|
||||
|
||||
!!! tip "Plugin Certification Program"
|
||||
NetBox Labs offers a [**Plugin Certification Program**](https://github.com/netbox-community/netbox/wiki/Plugin-Certification-Program) for plugin developers interested in establishing a co-maintainer relationship. The program aims to assure ongoing compatibility, maintainability, and commercial supportability of key plugins.
|
||||
|
||||
NetBox can be extended to support additional data models and functionality through the use of plugins. A plugin is essentially a self-contained [Django app](https://docs.djangoproject.com/en/stable/) which gets installed alongside NetBox to provide custom functionality. Multiple plugins can be installed in a single NetBox instance, and each plugin can be enabled and configured independently.
|
||||
|
||||
!!! info "Django Development"
|
||||
@@ -69,7 +72,7 @@ The plugin source directory contains all the actual Python code and other resour
|
||||
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
|
||||
|
||||
```python
|
||||
from extras.plugins import PluginConfig
|
||||
from netbox.plugins import PluginConfig
|
||||
|
||||
class FooBarConfig(PluginConfig):
|
||||
name = 'foo_bar'
|
||||
@@ -121,7 +124,7 @@ All required settings must be configured by the user. If a configuration paramet
|
||||
Plugin configuration parameters can be accessed using the `get_plugin_config()` function. For example:
|
||||
|
||||
```python
|
||||
from extras.plugins import get_plugin_config
|
||||
from netbox.plugins import get_plugin_config
|
||||
get_plugin_config('my_plugin', 'verbose_name')
|
||||
```
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
A plugin can register its own submenu as part of NetBox's navigation menu. This is done by defining a variable named `menu` in `navigation.py`, pointing to an instance of the `PluginMenu` class. Each menu must define a label and grouped menu items (discussed below), and may optionally specify an icon. An example is shown below.
|
||||
|
||||
```python title="navigation.py"
|
||||
from extras.plugins import PluginMenu
|
||||
from netbox.plugins import PluginMenu
|
||||
|
||||
menu = PluginMenu(
|
||||
label='My Plugin',
|
||||
@@ -49,7 +49,7 @@ menu_items = (item1, item2, item3)
|
||||
Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below.
|
||||
|
||||
```python title="navigation.py"
|
||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
||||
from netbox.plugins import PluginMenuButton, PluginMenuItem
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
item1 = PluginMenuItem(
|
||||
|
||||
@@ -157,7 +157,7 @@ These views are provided to enable or enhance certain NetBox model features, suc
|
||||
|
||||
### Additional Tabs
|
||||
|
||||
Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`:
|
||||
Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict:
|
||||
|
||||
```python
|
||||
from dcim.models import Site
|
||||
@@ -173,6 +173,16 @@ class MyView(generic.ObjectView):
|
||||
badge=lambda obj: Stuff.objects.filter(site=obj).count(),
|
||||
permission='myplugin.view_stuff'
|
||||
)
|
||||
|
||||
def get(self, request, pk):
|
||||
...
|
||||
return render(
|
||||
request,
|
||||
"myplugin/mytabview.html",
|
||||
context={
|
||||
"tab": self.tab,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
::: utilities.views.register_model_view
|
||||
@@ -206,7 +216,7 @@ For example, accessing `{{ request.user }}` within a template will return the cu
|
||||
Declared subclasses should be gathered into a list or tuple for integration with NetBox. By default, NetBox looks for an iterable named `template_extensions` within a `template_content.py` file. (This can be overridden by setting `template_extensions` to a custom value on the plugin's PluginConfig.) An example is below.
|
||||
|
||||
```python
|
||||
from extras.plugins import PluginTemplateExtension
|
||||
from netbox.plugins import PluginTemplateExtension
|
||||
from .models import Animal
|
||||
|
||||
class SiteAnimalCount(PluginTemplateExtension):
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Plugins are packaged [Django](https://docs.djangoproject.com/) apps that can be installed alongside NetBox to provide custom functionality not present in the core application. Plugins can introduce their own models and views, but cannot interfere with existing components. A NetBox user may opt to install plugins provided by the community or build his or her own.
|
||||
|
||||
Please see the documented instructions for [installing a plugin](./installation.md) to get started.
|
||||
|
||||
## Capabilities
|
||||
|
||||
The NetBox plugin architecture allows for the following:
|
||||
@@ -23,122 +25,3 @@ Either by policy or by technical limitation, the interaction of plugins with Net
|
||||
* **Override core templates.** Plugins can inject additional content where supported, but may not manipulate or remove core content.
|
||||
* **Modify core settings.** A configuration registry is provided for plugins, however they cannot alter or delete the core configuration.
|
||||
* **Disable core components.** Plugins are not permitted to disable or hide core NetBox components.
|
||||
|
||||
## Installing Plugins
|
||||
|
||||
The instructions below detail the process for installing and enabling a NetBox plugin.
|
||||
|
||||
### Install Package
|
||||
|
||||
Download and install the plugin package per its installation instructions. Plugins published via PyPI are typically installed using pip. Be sure to install the plugin within NetBox's virtual environment.
|
||||
|
||||
```no-highlight
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ pip install <package>
|
||||
```
|
||||
|
||||
Alternatively, you may wish to install the plugin manually by running `python setup.py install`. If you are developing a plugin and want to install it only temporarily, run `python setup.py develop` instead.
|
||||
|
||||
### Enable the Plugin
|
||||
|
||||
In `configuration.py`, add the plugin's name to the `PLUGINS` list:
|
||||
|
||||
```python
|
||||
PLUGINS = [
|
||||
'plugin_name',
|
||||
]
|
||||
```
|
||||
|
||||
### Configure Plugin
|
||||
|
||||
If the plugin requires any configuration, define it in `configuration.py` under the `PLUGINS_CONFIG` parameter. The available configuration parameters should be detailed in the plugin's README file.
|
||||
|
||||
```no-highlight
|
||||
PLUGINS_CONFIG = {
|
||||
'plugin_name': {
|
||||
'foo': 'bar',
|
||||
'buzz': 'bazz'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Run Database Migrations
|
||||
|
||||
If the plugin introduces new database models, run the provided schema migrations:
|
||||
|
||||
```no-highlight
|
||||
(venv) $ cd /opt/netbox/netbox/
|
||||
(venv) $ python3 manage.py migrate
|
||||
```
|
||||
|
||||
### Collect Static Files
|
||||
|
||||
Plugins may package static files to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
|
||||
|
||||
```no-highlight
|
||||
(venv) $ cd /opt/netbox/netbox/
|
||||
(venv) $ python3 manage.py collectstatic
|
||||
```
|
||||
|
||||
### Restart WSGI Service
|
||||
|
||||
Restart the WSGI service to load the new plugin:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
## Removing Plugins
|
||||
|
||||
Follow these steps to completely remove a plugin.
|
||||
|
||||
### Update Configuration
|
||||
|
||||
Remove the plugin from the `PLUGINS` list in `configuration.py`. Also remove any relevant configuration parameters from `PLUGINS_CONFIG`.
|
||||
|
||||
### Remove the Python Package
|
||||
|
||||
Use `pip` to remove the installed plugin:
|
||||
|
||||
```no-highlight
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ pip uninstall <package>
|
||||
```
|
||||
|
||||
### Restart WSGI Service
|
||||
|
||||
Restart the WSGI service:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
### Drop Database Tables
|
||||
|
||||
!!! note
|
||||
This step is necessary only for plugin which have created one or more database tables (generally through the introduction of new models). Check your plugin's documentation if unsure.
|
||||
|
||||
Enter the PostgreSQL database shell to determine if the plugin has created any SQL tables. Substitute `pluginname` in the example below for the name of the plugin being removed. (You can also run the `\dt` command without a pattern to list _all_ tables.)
|
||||
|
||||
```no-highlight
|
||||
netbox=> \dt pluginname_*
|
||||
List of relations
|
||||
List of relations
|
||||
Schema | Name | Type | Owner
|
||||
--------+----------------+-------+--------
|
||||
public | pluginname_foo | table | netbox
|
||||
public | pluginname_bar | table | netbox
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Exercise extreme caution when removing tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
|
||||
|
||||
Drop each of the listed tables to remove it from the database:
|
||||
|
||||
```no-highlight
|
||||
netbox=> DROP TABLE pluginname_foo;
|
||||
DROP TABLE
|
||||
netbox=> DROP TABLE pluginname_bar;
|
||||
DROP TABLE
|
||||
```
|
||||
|
||||
68
docs/plugins/installation.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Installing a Plugin
|
||||
|
||||
!!! warning
|
||||
The instructions below detail the general process for installing and configuring a NetBox plugin. However, each plugin is different and may require additional tasks or modifications to the steps below. Always consult the documentation for a specific plugin **before** attempting to install it.
|
||||
|
||||
## Install the Python Package
|
||||
|
||||
Download and install the plugin's Python package per its installation instructions. Plugins published via PyPI are typically installed using the [`pip`](https://packaging.python.org/en/latest/tutorials/installing-packages/) command line utility. Be sure to install the plugin within NetBox's virtual environment.
|
||||
|
||||
```no-highlight
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ pip install <package>
|
||||
```
|
||||
|
||||
Alternatively, you may wish to install the plugin manually by running `python setup.py install`. If you are developing a plugin and want to install it only temporarily, run `python setup.py develop` instead.
|
||||
|
||||
## Enable the Plugin
|
||||
|
||||
In `configuration.py`, add the plugin's name to the `PLUGINS` list:
|
||||
|
||||
```python
|
||||
PLUGINS = [
|
||||
# ...
|
||||
'plugin_name',
|
||||
]
|
||||
```
|
||||
|
||||
## Configure the Plugin
|
||||
|
||||
If the plugin requires any configuration, define it in `configuration.py` under the `PLUGINS_CONFIG` parameter. The available configuration parameters should be detailed in the plugin's `README` file or other documentation.
|
||||
|
||||
```no-highlight
|
||||
PLUGINS_CONFIG = {
|
||||
'plugin_name': {
|
||||
'foo': 'bar',
|
||||
'buzz': 'bazz'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Run Database Migrations
|
||||
|
||||
If the plugin introduces new database models, run the provided schema migrations:
|
||||
|
||||
```no-highlight
|
||||
(venv) $ cd /opt/netbox/netbox/
|
||||
(venv) $ python3 manage.py migrate
|
||||
```
|
||||
|
||||
!!! tip
|
||||
It's okay to run the `migrate` management command even if the plugin does not include any migration files.
|
||||
|
||||
## Collect Static Files
|
||||
|
||||
Plugins may package static resources like images or scripts to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
|
||||
|
||||
```no-highlight
|
||||
(venv) $ cd /opt/netbox/netbox/
|
||||
(venv) $ python3 manage.py collectstatic
|
||||
```
|
||||
|
||||
### Restart WSGI Service
|
||||
|
||||
Finally, restart the WSGI service and RQ workers to load the new plugin:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox netbox-rq
|
||||
```
|
||||
72
docs/plugins/removal.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Removing a Plugin
|
||||
|
||||
!!! warning
|
||||
The instructions below detail the general process for removing a NetBox plugin. However, each plugin is different and may require additional tasks or modifications to the steps below. Always consult the documentation for a specific plugin **before** attempting to remove it.
|
||||
|
||||
## Disable the Plugin
|
||||
|
||||
Disable the plugin by removing it from the `PLUGINS` list in `configuration.py`.
|
||||
|
||||
## Remove its Configuration
|
||||
|
||||
Delete the plugin's entry (if any) in the `PLUGINS_CONFIG` dictionary in `configuration.py`.
|
||||
|
||||
!!! tip
|
||||
If there's a chance you may reinstall the plugin, consider commenting out any configuration parameters instead of deleting them.
|
||||
|
||||
## Re-index Search Entries
|
||||
|
||||
Run the `reindex` management command to reindex the global search engine. This will remove any stale entries pertaining to objects provided by the plugin.
|
||||
|
||||
```no-highlight
|
||||
$ cd /opt/netbox/netbox/
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ python3 manage.py reindex
|
||||
```
|
||||
|
||||
## Uninstall its Python Package
|
||||
|
||||
Use `pip` to remove the installed plugin:
|
||||
|
||||
```no-highlight
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ pip uninstall <package>
|
||||
```
|
||||
|
||||
## Restart WSGI Service
|
||||
|
||||
Restart the WSGI service:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
## Drop Database Tables
|
||||
|
||||
!!! note
|
||||
This step is necessary only for plugins which have created one or more database tables (generally through the introduction of new models). Check your plugin's documentation if unsure.
|
||||
|
||||
Enter the PostgreSQL database shell (`manage.py dbshell`) to determine if the plugin has created any SQL tables. Substitute `pluginname` in the example below for the name of the plugin being removed. (You can also run the `\dt` command without a pattern to list _all_ tables.)
|
||||
|
||||
```no-highlight
|
||||
netbox=> \dt pluginname_*
|
||||
List of relations
|
||||
List of relations
|
||||
Schema | Name | Type | Owner
|
||||
--------+----------------+-------+--------
|
||||
public | pluginname_foo | table | netbox
|
||||
public | pluginname_bar | table | netbox
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Exercise extreme caution when removing tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
|
||||
|
||||
Drop each of the listed tables to remove it from the database:
|
||||
|
||||
```no-highlight
|
||||
netbox=> DROP TABLE pluginname_foo;
|
||||
DROP TABLE
|
||||
netbox=> DROP TABLE pluginname_bar;
|
||||
DROP TABLE
|
||||
```
|
||||
@@ -1,353 +1,254 @@
|
||||
---
|
||||
hide:
|
||||
- toc
|
||||
---
|
||||
|
||||
# Markdown
|
||||
|
||||
NetBox supports markdown rendering for certain text fields.
|
||||
NetBox supports Markdown rendering for certain text fields. Some common examples are provided below. For a complete Markdown reference, please see [Markdownguide.org](https://www.markdownguide.org/basic-syntax/).
|
||||
|
||||
## Syntax
|
||||
|
||||
##### Table of Contents
|
||||
[Headers](#headers)
|
||||
[Emphasis](#emphasis)
|
||||
[Lists](#lists)
|
||||
[Links](#links)
|
||||
[Images](#images)
|
||||
[Code Blocks](#code)
|
||||
[Tables](#tables)
|
||||
[Blockquotes](#blockquotes)
|
||||
[Inline HTML](#html)
|
||||
[Horizontal Rule](#hr)
|
||||
[Line Breaks](#lines)
|
||||
|
||||
<a name="headers"></a>
|
||||
|
||||
## Headers
|
||||
## Headings
|
||||
|
||||
```no-highlight
|
||||
# H1
|
||||
## H2
|
||||
### H3
|
||||
#### H4
|
||||
##### H5
|
||||
###### H6
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
```
|
||||
|
||||
<h1>Heading 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
<h4>Heading 4</h4>
|
||||
<h5>Heading 5</h5>
|
||||
<h6>Heading 6</h6>
|
||||
|
||||
Alternatively, for H1 and H2, an underline-ish style:
|
||||
|
||||
Alt-H1
|
||||
======
|
||||
```no-highlight
|
||||
Heading 1
|
||||
=========
|
||||
|
||||
Alt-H2
|
||||
------
|
||||
Heading 2
|
||||
---------
|
||||
```
|
||||
|
||||
# H1
|
||||
## H2
|
||||
### H3
|
||||
#### H4
|
||||
##### H5
|
||||
###### H6
|
||||
<h1>Heading 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
|
||||
<a name="emphasis"></a>
|
||||
|
||||
## Emphasis
|
||||
## Text
|
||||
|
||||
```no-highlight
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
Italicize text with *asterisks* or _underscores_.
|
||||
```
|
||||
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
|
||||
|
||||
<a name="lists"></a>
|
||||
|
||||
## Lists
|
||||
|
||||
(In this example, leading and trailing spaces are shown with with dots: ⋅)
|
||||
Italicize text with *asterisks* or _underscores_.
|
||||
|
||||
```no-highlight
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
⋅⋅* Unordered sub-list.
|
||||
1. Actual numbers don't matter, just that it's a number
|
||||
⋅⋅1. Ordered sub-list
|
||||
4. And another item.
|
||||
|
||||
⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅
|
||||
⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅
|
||||
⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Or minuses
|
||||
+ Or pluses
|
||||
Bold text with two **asterisks** or __underscores__.
|
||||
```
|
||||
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
* Unordered sub-list.
|
||||
1. Actual numbers don't matter, just that it's a number
|
||||
1. Ordered sub-list
|
||||
4. And another item.
|
||||
|
||||
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
To have a line break without a paragraph, you will need to use two trailing spaces.
|
||||
Note that this line is separate, but within the same paragraph.
|
||||
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Or minuses
|
||||
+ Or pluses
|
||||
|
||||
<a name="links"></a>
|
||||
|
||||
## Links
|
||||
|
||||
There are two ways to create links.
|
||||
Bold text with two **asterisks** or __underscores__.
|
||||
|
||||
```no-highlight
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||
|
||||
[I'm a reference-style link][Arbitrary case-insensitive reference text]
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself].
|
||||
|
||||
URLs and URLs in angle brackets will automatically get turned into links.
|
||||
http://www.example.com or <http://www.example.com> and sometimes
|
||||
example.com (but not on Github, for example).
|
||||
|
||||
Some text to show that the reference links can follow later.
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
||||
[1]: http://slashdot.org
|
||||
[link text itself]: http://www.reddit.com
|
||||
Strike text with two tildes. ~~Deleted text.~~
|
||||
```
|
||||
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||
|
||||
[I'm a reference-style link][Arbitrary case-insensitive reference text]
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself].
|
||||
|
||||
URLs and URLs in angle brackets will automatically get turned into links.
|
||||
http://www.example.com or <http://www.example.com> and sometimes
|
||||
example.com (but not on Github, for example).
|
||||
|
||||
Some text to show that the reference links can follow later.
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
||||
[1]: http://slashdot.org
|
||||
[link text itself]: http://www.reddit.com
|
||||
|
||||
<a name="images"></a>
|
||||
|
||||
## Images
|
||||
|
||||
```
|
||||
Here's the NetBox logo (hover to see the title text):
|
||||
|
||||
Inline-style:
|
||||

|
||||
|
||||
Reference-style:
|
||||
![alt text][logo]
|
||||
|
||||
[logo]: /media/misc/netbox_logo.png "Logo Title Text 2"
|
||||
```
|
||||
|
||||
Here's the NetBox logo (hover to see the title text):
|
||||
|
||||
Inline-style:
|
||||

|
||||
|
||||
Reference-style:
|
||||
![alt text][logo]
|
||||
|
||||
[logo]: ../media/misc/netbox_logo.png "Logo Title Text 2"
|
||||
|
||||
<a name="code"></a>
|
||||
|
||||
## Code blocks
|
||||
|
||||
```
|
||||
Inline `code` has `back-ticks around` it.
|
||||
```
|
||||
|
||||
Inline `code` has `back-ticks around` it.
|
||||
|
||||
Blocks of code are fenced by lines with three back-ticks <code>```</code>
|
||||
|
||||
````
|
||||
```
|
||||
var s = "Code block";
|
||||
alert(s);
|
||||
```
|
||||
````
|
||||
|
||||
```
|
||||
var s = "Code block";
|
||||
alert(s);
|
||||
```
|
||||
|
||||
<a name="tables"></a>
|
||||
|
||||
## Tables
|
||||
|
||||
```no-highlight
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- |:-------------:| -----:|
|
||||
| col 3 is | right-aligned | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell.
|
||||
The outer pipes (|) are optional, and you don't need to make the
|
||||
raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
Markdown | Less | Pretty
|
||||
--- | --- | ---
|
||||
*Still* | `renders` | **nicely**
|
||||
1 | 2 | 3
|
||||
```
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- |:-------------:| -----:|
|
||||
| col 3 is | right-aligned | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
Markdown | Less | Pretty
|
||||
--- | --- | ---
|
||||
*Still* | `renders` | **nicely**
|
||||
1 | 2 | 3
|
||||
|
||||
<a name="blockquotes"></a>
|
||||
|
||||
## Blockquotes
|
||||
|
||||
```no-highlight
|
||||
> Blockquotes are very handy in email to emulate reply text.
|
||||
> This line is part of the same quote.
|
||||
|
||||
Quote break.
|
||||
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
|
||||
```
|
||||
|
||||
> Blockquotes are very handy in email to emulate reply text.
|
||||
> This line is part of the same quote.
|
||||
|
||||
Quote break.
|
||||
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
|
||||
|
||||
<a name="html"></a>
|
||||
|
||||
## Inline HTML
|
||||
|
||||
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
|
||||
|
||||
```no-highlight
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
<dd>Is something people use sometimes.</dd>
|
||||
|
||||
<dt>Markdown in HTML</dt>
|
||||
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
|
||||
</dl>
|
||||
```
|
||||
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
<dd>Is something people use sometimes.</dd>
|
||||
|
||||
<dt>Markdown in HTML</dt>
|
||||
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
|
||||
</dl>
|
||||
|
||||
<a name="hr"></a>
|
||||
|
||||
## Horizontal Rule
|
||||
|
||||
```
|
||||
Three or more...
|
||||
|
||||
---
|
||||
|
||||
Hyphens
|
||||
|
||||
***
|
||||
|
||||
Asterisks
|
||||
|
||||
___
|
||||
|
||||
Underscores
|
||||
```
|
||||
|
||||
Three or more...
|
||||
|
||||
---
|
||||
|
||||
Hyphens
|
||||
|
||||
***
|
||||
|
||||
Asterisks
|
||||
|
||||
___
|
||||
|
||||
Underscores
|
||||
|
||||
<a name="lines"></a>
|
||||
Strike text with two tildes. ~~Deleted text.~~
|
||||
|
||||
## Line Breaks
|
||||
|
||||
By default, Markdown will remove line breaks between successive lines of text. For example:
|
||||
|
||||
```
|
||||
Here's a line for us to start with.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
||||
|
||||
This line is also a separate paragraph, but...
|
||||
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
|
||||
```no-highlight
|
||||
This is one line.
|
||||
And this is another line.
|
||||
One more line here.
|
||||
```
|
||||
|
||||
Here's a line for us to start with.
|
||||
This is one line.
|
||||
And this is another line.
|
||||
One more line here.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
||||
To preserve line breaks, append two spaces to each line (represented below with the `⋅` character).
|
||||
|
||||
This line is also begins a separate paragraph, but...
|
||||
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
|
||||
```no-highlight
|
||||
This is one line.⋅⋅
|
||||
And this is another line.⋅⋅
|
||||
One more line here.
|
||||
```
|
||||
|
||||
Based on [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) by [adam-p](https://github.com/adam-p) licensed under [CC-BY](https://creativecommons.org/licenses/by/3.0/)
|
||||
This is one line.
|
||||
And this is another line.
|
||||
One more line here.
|
||||
|
||||
## Lists
|
||||
|
||||
Use asterisks or hyphens for unordered lists. Indent items by four spaces to start a child list.
|
||||
|
||||
```no-highlight
|
||||
* Alpha
|
||||
* Bravo
|
||||
* Charlie
|
||||
* Child item 1
|
||||
* Child item 2
|
||||
* Delta
|
||||
```
|
||||
|
||||
* Alpha
|
||||
* Bravo
|
||||
* Charlie
|
||||
* Child item 1
|
||||
* Child item 2
|
||||
* Delta
|
||||
|
||||
Use digits followed by periods for ordered (numbered) lists.
|
||||
|
||||
```no-highlight
|
||||
1. Red
|
||||
2. Green
|
||||
3. Blue
|
||||
1. Light blue
|
||||
2. Dark blue
|
||||
4. Orange
|
||||
```
|
||||
|
||||
1. Red
|
||||
2. Green
|
||||
3. Blue
|
||||
1. Light blue
|
||||
2. Dark blue
|
||||
4. Orange
|
||||
|
||||
## Links
|
||||
|
||||
Text can be rendered as a hyperlink by encasing it in square brackets, followed by a URL in parentheses. A title (text displayed on hover) may optionally be included as well.
|
||||
|
||||
```no-highlight
|
||||
Here's an [example](https://www.example.com) of a link.
|
||||
|
||||
And here's [another link](https://www.example.com "Click me!"), this time with a title.
|
||||
```
|
||||
|
||||
Here's an [example](https://www.example.com) of a link.
|
||||
|
||||
And here's [another link](https://www.example.com "Click me!"), with a title.
|
||||
|
||||
## Images
|
||||
|
||||
The syntax for embedding an image is very similar to that used for a hyperlink. Alternate text should always be provided; this will be displayed if the image fails to load. As with hyperlinks, title text is optional.
|
||||
|
||||
```no-highlight
|
||||

|
||||
```
|
||||
|
||||
## Code Blocks
|
||||
|
||||
Single backticks can be used to annotate code inline. Text enclosed by lines of three backticks will be displayed as a code block.
|
||||
|
||||
```no-highlight
|
||||
Paragraphs are rendered in HTML using `<p>` and `</p>` tags.
|
||||
```
|
||||
|
||||
Paragraphs are rendered in HTML using `<p>` and `</p>` tags.
|
||||
|
||||
````
|
||||
```
|
||||
def my_func(foo, bar):
|
||||
# Do something
|
||||
return foo * bar
|
||||
```
|
||||
````
|
||||
|
||||
```no-highlight
|
||||
def my_func(foo, bar):
|
||||
# Do something
|
||||
return foo * bar
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
Simple tables can be constructed using the pipe character (`|`) to denote columns, and hyphens (`-`) to denote the heading. Inline Markdown can be used to style text within columns.
|
||||
|
||||
```no-highlight
|
||||
| Heading 1 | Heading 2 | Heading 3 |
|
||||
|-----------|-----------|-----------|
|
||||
| Row 1 | Alpha | Red |
|
||||
| Row 2 | **Bravo** | Green |
|
||||
| Row 3 | Charlie | ~~Blue~~ |
|
||||
```
|
||||
|
||||
| Heading 1 | Heading 2 | Heading 3 |
|
||||
|-----------|-----------|-----------|
|
||||
| _Row 1_ | Alpha | Red |
|
||||
| Row 2 | **Bravo** | Green |
|
||||
| Row 3 | Charlie | ~~Blue~~ |
|
||||
|
||||
Colons can be used to align text to the left or right side of a column.
|
||||
|
||||
```no-highlight
|
||||
| Left-aligned | Centered | Right-aligned |
|
||||
|:-------------|:--------:|--------------:|
|
||||
| Text | Text | Text |
|
||||
| Text | Text | Text |
|
||||
| Text | Text | Text |
|
||||
```
|
||||
|
||||
| Left-aligned | Centered | Right-aligned |
|
||||
|:-------------|:--------:|--------------:|
|
||||
| Text | Text | Text |
|
||||
| Text | Text | Text |
|
||||
| Text | Text | Text |
|
||||
|
||||
## Blockquotes
|
||||
|
||||
Text can be wrapped in a blockquote by prepending a right angle bracket (`>`) before each line.
|
||||
|
||||
```no-highlight
|
||||
> I think that I shall never see
|
||||
> a graph more lovely than a tree.
|
||||
> A tree whose crucial property
|
||||
> is loop-free connectivity.
|
||||
```
|
||||
|
||||
> I think that I shall never see
|
||||
> a graph more lovely than a tree.
|
||||
> A tree whose crucial property
|
||||
> is loop-free connectivity.
|
||||
|
||||
Markdown removes line breaks by default. To preserve line breaks, append two spaces to each line (represented below with the `⋅` character).
|
||||
|
||||
```no-highlight
|
||||
> I think that I shall never see⋅⋅
|
||||
> a graph more lovely than a tree.⋅⋅
|
||||
> A tree whose crucial property⋅⋅
|
||||
> is loop-free connectivity.
|
||||
```
|
||||
|
||||
> I think that I shall never see
|
||||
> a graph more lovely than a tree.
|
||||
> A tree whose crucial property
|
||||
> is loop-free connectivity.
|
||||
|
||||
## Horizontal Rule
|
||||
|
||||
A horizontal rule is a single line rendered across the width of the page using a series of three or more hyphens or asterisks. It can be useful for separating sections of content.
|
||||
|
||||
```no-highlight
|
||||
Content
|
||||
|
||||
---
|
||||
|
||||
More content
|
||||
|
||||
***
|
||||
|
||||
Final content
|
||||
```
|
||||
|
||||
Content
|
||||
|
||||
---
|
||||
|
||||
More content
|
||||
|
||||
***
|
||||
|
||||
Final content
|
||||
|
||||
@@ -10,7 +10,7 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 3.7](./version-3.6.md) (December 2023)
|
||||
#### [Version 3.7](./version-3.7.md) (December 2023)
|
||||
|
||||
* VPN Tunnels ([#9816](https://github.com/netbox-community/netbox/issues/9816))
|
||||
* Event Rules ([#14132](https://github.com/netbox-community/netbox/issues/14132))
|
||||
|
||||
@@ -1,6 +1,69 @@
|
||||
# NetBox v3.6
|
||||
|
||||
## v3.6.7 (FUTURE)
|
||||
## v3.6.9 (2023-12-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14631](https://github.com/netbox-community/netbox/issues/14631) - All models can be filtered and searched by their description field (where applicable)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14482](https://github.com/netbox-community/netbox/issues/14482) - Fix validation error when attempting to move a primary IP address to a new parent object
|
||||
* [#14620](https://github.com/netbox-community/netbox/issues/14620) - Permit setting device type U height to 0 during bulk edit
|
||||
* [#14621](https://github.com/netbox-community/netbox/issues/14621) - Fix error when using the device search filter
|
||||
|
||||
---
|
||||
|
||||
## v3.6.8 (2023-12-27)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#11039](https://github.com/netbox-community/netbox/issues/11039) - List parent prefixes under IP range view
|
||||
* [#14507](https://github.com/netbox-community/netbox/issues/14507) - Print new NetBox version when running upgrade script
|
||||
* [#14538](https://github.com/netbox-community/netbox/issues/14538) - Add the `available_at_site` filter for VLANs
|
||||
* [#14596](https://github.com/netbox-community/netbox/issues/14596) - Match against description field when searching for devices
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#11816](https://github.com/netbox-community/netbox/issues/11816) - Correct display of error message when attempting invalid VLAN site & group assignment
|
||||
* [#12731](https://github.com/netbox-community/netbox/issues/12731) - Fix custom validation for many-to-many fields
|
||||
* [#13606](https://github.com/netbox-community/netbox/issues/13606) - Fix filtering custom multi-choice fields by null
|
||||
* [#13649](https://github.com/netbox-community/netbox/issues/13649) - Correct calculation of absolute lengths for zero-length cables
|
||||
* [#13812](https://github.com/netbox-community/netbox/issues/13812) - Update status of remote data source when syncing fails via `syncdatasource` management command
|
||||
* [#13909](https://github.com/netbox-community/netbox/issues/13909) - Fix cloning of objects which have a multi-choice custom field
|
||||
* [#14517](https://github.com/netbox-community/netbox/issues/14517) - Ensure reservations tab is always displayed under rack view
|
||||
* [#14532](https://github.com/netbox-community/netbox/issues/14532) - Device/VM change record should accurately reflect when primary/OOB IP is deleted
|
||||
* [#14549](https://github.com/netbox-community/netbox/issues/14549) - Fix association of job results when executing scripts via `runscript` management command
|
||||
* [#14560](https://github.com/netbox-community/netbox/issues/14560) - Do not escape exclamation marks in custom link URLs
|
||||
* [#14575](https://github.com/netbox-community/netbox/issues/14575) - Fix display of the tags column under VDC table
|
||||
* [#14613](https://github.com/netbox-community/netbox/issues/14613) - Fix display of current configuration parameters in UI
|
||||
|
||||
---
|
||||
|
||||
## v3.6.7 (2023-12-15)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#12751](https://github.com/netbox-community/netbox/issues/12751) - Designate fields to expand by default for object selector widget
|
||||
* [#14148](https://github.com/netbox-community/netbox/issues/14148) - Add tags column to L2VPN terminations column
|
||||
* [#14390](https://github.com/netbox-community/netbox/issues/14390) - Add `classes` parameter to `copy_content` template tag
|
||||
* [#14467](https://github.com/netbox-community/netbox/issues/14467) - Change custom field choice delimiter from comma to colon
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#13983](https://github.com/netbox-community/netbox/issues/13983) - Fix bulk import support for custom field choices
|
||||
* [#14081](https://github.com/netbox-community/netbox/issues/14081) - Ensure accuracy of parent object counters when deleting related objects
|
||||
* [#14249](https://github.com/netbox-community/netbox/issues/14249) - Fix server error when authenticating via IP-restricted API tokens using IPv6
|
||||
* [#14392](https://github.com/netbox-community/netbox/issues/14392) - Fix bulk operations for plugin models under admin UI
|
||||
* [#14397](https://github.com/netbox-community/netbox/issues/14397) - Fix exception on non-JSON request to `/available-ips/` API endpoints
|
||||
* [#14401](https://github.com/netbox-community/netbox/issues/14401) - Rack `starting_unit` cannot be zero
|
||||
* [#14432](https://github.com/netbox-community/netbox/issues/14432) - Populate custom field default values for components when creating a device
|
||||
* [#14448](https://github.com/netbox-community/netbox/issues/14448) - Fix exception when creating a power feed with rack and panel in different sites
|
||||
* [#14505](https://github.com/netbox-community/netbox/issues/14505) - Fix the assignment of tags to L2VPN terminations
|
||||
* [#14512](https://github.com/netbox-community/netbox/issues/14512) - Remove unneeded annotations from queries when using REST API brief mode
|
||||
* [#14515](https://github.com/netbox-community/netbox/issues/14515) - Ensure user config is created automatically for all user accounts
|
||||
* [#14522](https://github.com/netbox-community/netbox/issues/14522) - Fix filtering contact assignments by group
|
||||
* [#14533](https://github.com/netbox-community/netbox/issues/14533) - Fix quick search under VLAN group VLANs list
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,37 +1,196 @@
|
||||
## v3.7-beta1 (2023-12-05)
|
||||
# NetBox v3.7
|
||||
|
||||
## v3.7.6 (2024-04-22)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14690](https://github.com/netbox-community/netbox/issues/14690) - Improve rendering of JSON data in configuration form
|
||||
* [#15427](https://github.com/netbox-community/netbox/issues/15427) - Enable compatibility with non-Amazon S3 providers for remote data sources
|
||||
* [#15640](https://github.com/netbox-community/netbox/issues/15640) - Add global search support for L2VPN identifiers
|
||||
* [#15644](https://github.com/netbox-community/netbox/issues/15644) - Introduce new configuration parameters for enabling HTTP Strict Transport Security (HSTS)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#15541](https://github.com/netbox-community/netbox/issues/15541) - Restore ability to modify assigned component template when adding/modifying an inventory item template
|
||||
* [#15582](https://github.com/netbox-community/netbox/issues/15582) - Fix permission constraints for synchronization of remote data sources
|
||||
* [#15588](https://github.com/netbox-community/netbox/issues/15588) - Correct OpenAPI schema definitions for read-only fields which may return null values
|
||||
* [#15635](https://github.com/netbox-community/netbox/issues/15635) - Extend plugin removal instruction to include reindexing the global search cache
|
||||
* [#15654](https://github.com/netbox-community/netbox/issues/15654) - Fix `AttributeError` exception when attempting to save an incomplete tunnel termination
|
||||
* [#15668](https://github.com/netbox-community/netbox/issues/15668) - Fix permission required to display virtual disks tab on virtual machine UI view
|
||||
* [#15685](https://github.com/netbox-community/netbox/issues/15685) - Allow filtering cables by decimal values using UI filter form
|
||||
* [#15761](https://github.com/netbox-community/netbox/issues/15761) - Add missing `ike_policy` & `ike_policy_id` filters for IKE proposals
|
||||
* [#15771](https://github.com/netbox-community/netbox/issues/15771) - Include `id` in list of supported fields for all bulk import forms
|
||||
* [#15790](https://github.com/netbox-community/netbox/issues/15790) - Fix live preview support for EventRule comments
|
||||
|
||||
---
|
||||
|
||||
## v3.7.5 (2024-04-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14707](https://github.com/netbox-community/netbox/issues/14707) - Clarify interface designation when creating tunnel terminations
|
||||
* [#15039](https://github.com/netbox-community/netbox/issues/15039) - Allow API tokens to be cloned
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14799](https://github.com/netbox-community/netbox/issues/14799) - Avoid caching modified reports & scripts
|
||||
* [#15029](https://github.com/netbox-community/netbox/issues/15029) - Raise a clean validation error when attempting to make duplicate FHRP group assignments
|
||||
* [#15102](https://github.com/netbox-community/netbox/issues/15102) - Fix usage of selector widget for form fields referencing users/groups
|
||||
* [#15435](https://github.com/netbox-community/netbox/issues/15435) - Correct permissions name to allow adding a module bay to a device via the UI
|
||||
* [#15502](https://github.com/netbox-community/netbox/issues/15502) - Fix KeyError exception when modifying an IP address assigned to a virtual machine
|
||||
* [#15597](https://github.com/netbox-community/netbox/issues/15597) - Restore help modal for `button_class` field on custom link bulk import form
|
||||
* [#15598](https://github.com/netbox-community/netbox/issues/15598) - Fix exception when creating a device from a device type with one or more child inventory items
|
||||
* [#15608](https://github.com/netbox-community/netbox/issues/15608) - Avoid caching values of null fields in search index
|
||||
* [#15609](https://github.com/netbox-community/netbox/issues/15609) - Fix filtering of the providers list by assigned ASN
|
||||
|
||||
---
|
||||
|
||||
## v3.7.4 (2024-03-13)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14206](https://github.com/netbox-community/netbox/issues/14206) - Add additional FibreChannel SFP+ interface types
|
||||
* [#14366](https://github.com/netbox-community/netbox/issues/14366) - Enable custom links for config contexts & templates
|
||||
* [#15291](https://github.com/netbox-community/netbox/issues/15291) - Add tunnel termination buttons to VM interfaces table
|
||||
* [#15297](https://github.com/netbox-community/netbox/issues/15297) - Linkify platform column in device & virtual machine tables
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#13722](https://github.com/netbox-community/netbox/issues/13722) - Fix range expansion for comma-separated numerical values
|
||||
* [#14832](https://github.com/netbox-community/netbox/issues/14832) - Enable querying IP addresses for an FHRP group via GraphQL
|
||||
* [#15220](https://github.com/netbox-community/netbox/issues/15220) - Fix validation check when bulk editing the mask length of IP addresses
|
||||
* [#15232](https://github.com/netbox-community/netbox/issues/15232) - Permit user with sufficient permissions to assign an inventory item to a device type
|
||||
* [#15241](https://github.com/netbox-community/netbox/issues/15241) - Restore missing `display` field on VirtualDisk serialization in REST API
|
||||
* [#15243](https://github.com/netbox-community/netbox/issues/15243) - Correct representation of installed module when listing module bays using REST API brief mode
|
||||
* [#15316](https://github.com/netbox-community/netbox/issues/15316) - Fix selection of 3DES encryption for IKE & IPSec proposals
|
||||
* [#15322](https://github.com/netbox-community/netbox/issues/15322) - Add description field to YAML export for device & module types
|
||||
* [#15336](https://github.com/netbox-community/netbox/issues/15336) - Correct label for recurring scheduled jobs
|
||||
* [#15347](https://github.com/netbox-community/netbox/issues/15347) - Fix querying virtual machine contacts via GraphQL
|
||||
* [#15356](https://github.com/netbox-community/netbox/issues/15356) - Fix assignment of front & rear images to device types via REST API
|
||||
|
||||
---
|
||||
|
||||
## v3.7.3 (2024-02-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14587](https://github.com/netbox-community/netbox/issues/14587) - Display a human-friendly name for the OpenID Connect remote auth backend
|
||||
* [#14946](https://github.com/netbox-community/netbox/issues/14946) - Remove `associate_by_email()` from default social auth pipeline
|
||||
* [#14966](https://github.com/netbox-community/netbox/issues/14966) - Add PostgreSQL index for object type & ID on CachedValue table to improve performance
|
||||
* [#15177](https://github.com/netbox-community/netbox/issues/15177) - Add "last login" time to user display & REST API serializer
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14058](https://github.com/netbox-community/netbox/issues/14058) - Limit platform options by manufacturer when editing a device or device type
|
||||
* [#14064](https://github.com/netbox-community/netbox/issues/14064) - Resolving parent location should consider assigned site when bulk importing locations
|
||||
* [#14079](https://github.com/netbox-community/netbox/issues/14079) - Ensure changes are logged on related objects when deleting an object referenced via a many-to-many relationship (e.g. tags)
|
||||
* [#14405](https://github.com/netbox-community/netbox/issues/14405) - Clean up formatting of link peers in bulk CSV export of cable termination objects
|
||||
* [#14689](https://github.com/netbox-community/netbox/issues/14689) - Preserve "empty" default values for JSON custom fields
|
||||
* [#14952](https://github.com/netbox-community/netbox/issues/14952) - Update existing AutoSyncRecord when changing the data file of an auto-synced object
|
||||
* [#15059](https://github.com/netbox-community/netbox/issues/15059) - Correct IP address count link in VM interfaces table
|
||||
* [#15067](https://github.com/netbox-community/netbox/issues/15067) - Fix uncaught exception when attempting invalid device bay import
|
||||
* [#15070](https://github.com/netbox-community/netbox/issues/15070) - Fix inclusion of `config_template` field on REST API serializer for virtual machines
|
||||
* [#15084](https://github.com/netbox-community/netbox/issues/15084) - Fix "add export template" link under "export" button on object list views
|
||||
* [#15090](https://github.com/netbox-community/netbox/issues/15090) - Ensure protection rules are evaluated prior to enqueueing events when deleting an object
|
||||
* [#15091](https://github.com/netbox-community/netbox/issues/15091) - Fix designation of the active tab for assigned object when modifying an L2VPN termination
|
||||
* [#15101](https://github.com/netbox-community/netbox/issues/15101) - Correct OpenAPI schema for rack elevation REST API endpoint
|
||||
* [#15115](https://github.com/netbox-community/netbox/issues/15115) - Fix unhandled exception with invalid permission constraints
|
||||
* [#15126](https://github.com/netbox-community/netbox/issues/15126) - `group` field should be optional when creating VPN tunnel via REST API
|
||||
* [#15127](https://github.com/netbox-community/netbox/issues/15127) - Add missing group column to VPN tunnels table
|
||||
* [#15133](https://github.com/netbox-community/netbox/issues/15133) - Fix FHRP group representation on assignments REST API endpoint using brief mode
|
||||
* [#15174](https://github.com/netbox-community/netbox/issues/15174) - Warn that permission constraints are not supported for reports or scripts
|
||||
* [#15184](https://github.com/netbox-community/netbox/issues/15184) - Correct REST API schema definition for `front_image` & `rear_image` on DeviceType
|
||||
* [#15185](https://github.com/netbox-community/netbox/issues/15185) - Ensure error messages pertaining to related objects are displayed on the bulk import form
|
||||
* [#15192](https://github.com/netbox-community/netbox/issues/15192) - Fix exception when viewing current config when no history is present
|
||||
|
||||
---
|
||||
|
||||
## v3.7.2 (2024-02-05)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#13729](https://github.com/netbox-community/netbox/issues/13729) - Omit sensitive data source parameters from change log data
|
||||
* [#14645](https://github.com/netbox-community/netbox/issues/14645) - Limit the number of assigned IP addresses displayed under interfaces list
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14500](https://github.com/netbox-community/netbox/issues/14500) - Optimize calculation of available child prefixes & ranges when viewing a prefix
|
||||
* [#14511](https://github.com/netbox-community/netbox/issues/14511) - Fix GraphQL support for interfaces connected to provider networks
|
||||
* [#14572](https://github.com/netbox-community/netbox/issues/14572) - Correct the number of jobs listed for individual report & script modules
|
||||
* [#14703](https://github.com/netbox-community/netbox/issues/14703) - Revert to the default layout when encountering a misconfigured dashboard
|
||||
* [#14755](https://github.com/netbox-community/netbox/issues/14755) - Fix validation of choice values & labels when creating a custom field choice set via the REST API
|
||||
* [#14838](https://github.com/netbox-community/netbox/issues/14838) - Avoid corrupting JSON data when changing the action type while editing an event rule
|
||||
* [#14839](https://github.com/netbox-community/netbox/issues/14839) - Fix form validation error when attempting to terminate a tunnel to a virtual machine interface
|
||||
* [#14840](https://github.com/netbox-community/netbox/issues/14840) - Fix `NoReverseMatch` exception when rendering a custom field which references a user
|
||||
* [#14847](https://github.com/netbox-community/netbox/issues/14847) - IKE policy mode may be set inly when IKEv1 is selected
|
||||
* [#14851](https://github.com/netbox-community/netbox/issues/14851) - Automatically remove any associated bookmarks when deleting a user
|
||||
* [#14879](https://github.com/netbox-community/netbox/issues/14879) - Include custom fields in REST API representation of data sources
|
||||
* [#14885](https://github.com/netbox-community/netbox/issues/14885) - Add missing "group" field to VPN tunnel creation form
|
||||
* [#14892](https://github.com/netbox-community/netbox/issues/14892) - Fix exception when running report/script via command line due to missing username
|
||||
* [#14920](https://github.com/netbox-community/netbox/issues/14920) - Include button to display available status choices when bulk importing virtual device contexts
|
||||
* [#14945](https://github.com/netbox-community/netbox/issues/14945) - Fix "select all" button for device type components
|
||||
* [#14947](https://github.com/netbox-community/netbox/issues/14947) - Ensure that application & removal of tags is always recorded in an object's change log
|
||||
* [#14962](https://github.com/netbox-community/netbox/issues/14962) - Fix config context rendering for VMs assigned directly to a site (rather than via a cluster)
|
||||
* [#14999](https://github.com/netbox-community/netbox/issues/14999) - Fix "create & add another" link for interface FHRP group assignment
|
||||
* [#15015](https://github.com/netbox-community/netbox/issues/15015) - Pre-populate assigned tenant when allocating next available IP address under prefix view
|
||||
* [#15020](https://github.com/netbox-community/netbox/issues/15020) - Automatically update all VMs when changing a cluster's assigned site
|
||||
* [#15025](https://github.com/netbox-community/netbox/issues/15025) - The `can_add()` template filter should accept a model (not an instance)
|
||||
|
||||
---
|
||||
|
||||
## v3.7.1 (2024-01-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#13844](https://github.com/netbox-community/netbox/issues/13844) - Use `available_at_site` filter when filtering VLANs under prefix form
|
||||
* [#14663](https://github.com/netbox-community/netbox/issues/14663) - Fix tunnel creation when setting initial termination to a VM interface
|
||||
* [#14706](https://github.com/netbox-community/netbox/issues/14706) - Relax one-to-one mapping of tunnel termination to IP address
|
||||
* [#14709](https://github.com/netbox-community/netbox/issues/14709) - Fix typo in tunnel termination type choice name
|
||||
* [#14749](https://github.com/netbox-community/netbox/issues/14749) - Remove errant translation wrapper from `installed_device` on DeviceBay
|
||||
* [#14778](https://github.com/netbox-community/netbox/issues/14778) - Custom field API serializer should accept null values for all optional fields
|
||||
* [#14791](https://github.com/netbox-community/netbox/issues/14791) - Hide available prefixes when searching within a parent prefix
|
||||
* [#14793](https://github.com/netbox-community/netbox/issues/14793) - Add missing Diffie-Hellman group 15
|
||||
* [#14816](https://github.com/netbox-community/netbox/issues/14816) - Ensure default contact assignment ordering is consistent
|
||||
* [#14817](https://github.com/netbox-community/netbox/issues/14817) - Relax required fields for IKE & IPSec models on bulk import
|
||||
* [#14827](https://github.com/netbox-community/netbox/issues/14827) - Ensure all matching event rules are processed in response to an event
|
||||
|
||||
---
|
||||
|
||||
## v3.7.0 (2023-12-29)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The following fields have been removed from the Webhook model: `content_types`, `type_create`, `type_update`, `type_delete`, `type_job_start`, `type_job_end`, `enabled`, and `conditions`. Webhooks are now tied to events via [event rules](../features/event-rules.md). Existing webhooks will have event rules created automatically upon upgrade.
|
||||
* The `ui_visibility` field on the [custom field model](../models/extras/customfield.md) has been replaced with two new fields: `ui_visible` and `ui_editable`. Existing values will be migrated automatically upon upgrade.
|
||||
* The `FeatureQuery` class for querying content types by model feature has been removed. Plugins should now use the new `with_feature()` manager method on NetBox's proxy model for ContentType.
|
||||
* The ConfigRevision model has been moved from `extras` to `core`. Configuration history will be retained throughout the upgrade process.
|
||||
* The L2VPN and L2VPNTermination models have been moved from the `ipam` app to the new `vpn` app. All object data will be retained, however please note that the relevant API endpoints have moved to `/api/vpn/`.
|
||||
* The following fields have been removed from the Webhook model: `content_types`, `type_create`, `type_update`, `type_delete`, `type_job_start`, `type_job_end`, `enabled`, and `conditions`. Webhooks are now tied to events via [event rules](../features/event-rules.md). New event rules will be created for any existing webhooks automatically upon upgrade.
|
||||
* The `ui_visibility` field on the [custom field model](../models/extras/customfield.md) has been replaced with two new fields: `ui_visible` and `ui_editable`. These new fields will have their values mapped from the original field automatically upon upgrade.
|
||||
* The `FeatureQuery` class used internally for querying content types by model feature has been removed. It has been replaced by the new `with_feature()` manager method on NetBox's proxy model for ContentType (`core.models.ContentType`).
|
||||
* The internal ConfigRevision model has moved from `extras` to `core`. Configuration history will be retained throughout the upgrade process.
|
||||
* The [L2VPN](../models/vpn/l2vpn.md) and [L2VPNTermination](../models/vpn/l2vpntermination.md) models have moved from the `ipam` app to the new `vpn` app. All object data will be retained, however please note that the relevant API endpoints have likewise moved to `/api/vpn/`.
|
||||
* The `CustomFieldsMixin`, `SavedFiltersMixin`, and `TagsMixin` classes have moved from the `extras.forms.mixins` module to `netbox.forms.mixins`.
|
||||
* The `netbox.models.features.WebhooksMixin` class has been renamed to `EventRulesMixin`.
|
||||
|
||||
### New Features
|
||||
|
||||
#### VPN Tunnels ([#9816](https://github.com/netbox-community/netbox/issues/9816))
|
||||
|
||||
Several new models have been introduced to enable [VPN tunnel management](../features/vpn-tunnels.md). Users can now define tunnels with two or more terminations to replicate peer-to-peer or hub-and-spoke topologies. Each termination is made to a virtual interface on a device or VM. Additionally, users can define IKE and IPSec policies which can be applied to tunnels to document encryption and authentication strategies.
|
||||
Several new models have been introduced to enable [VPN tunnel management](../features/vpn-tunnels.md). Users can now define tunnels with two or more terminations to represent peer-to-peer or hub-and-spoke topologies. Each termination is made to a virtual interface on a device or virtual machine. Additionally, users can define IKE and IPSec proposals and policies, which can be applied to tunnels to document encryption and authentication strategies.
|
||||
|
||||
#### Event Rules ([#14132](https://github.com/netbox-community/netbox/issues/14132))
|
||||
|
||||
This release introduces [event rules](../features/event-rules.md), which can be used to send webhooks or execute custom scripts automatically in response to NetBox events. For example, it's now possible to run a custom script whenever a new site is created with a particular status or tag.
|
||||
This release introduces [event rules](../features/event-rules.md), which can be used to send webhooks or execute custom scripts automatically in response to events that occur in NetBox. For example, it's now possible to run a custom script whenever a new site is created with a particular status or tag.
|
||||
|
||||
Event rules replace and extend functionality that was previously built into the webhook model. Event rules will be created for any existing webhooks upon upgrade.
|
||||
Event rules replace and extend functionality that was previously built into the webhook model. New event rules will be created for any existing webhooks automatically upon upgrade.
|
||||
|
||||
#### Virtual Machine Disks ([#8356](https://github.com/netbox-community/netbox/issues/8356))
|
||||
|
||||
A new [VirtualDisk](../models/virtualization/virtualdisk.md) model has been introduced to enable tracking the assignment of discrete virtual disks to virtual machines. The original `size` field has been retained on the VirtualMachine model, and will be automatically updated with the aggregate size of all assigned virtual disks. (Users who opt to eschew the new model may continue using the VirtualMachine `size` attribute as before.)
|
||||
A new [VirtualDisk](../models/virtualization/virtualdisk.md) model has been introduced to enable tracking the assignment of discrete virtual disks to virtual machines. The `size` field has been retained on the VirtualMachine model, and will be populated automatically with the aggregate size of all assigned virtual disks. (Users who opt to eschew the new model may continue using the VirtualMachine `size` attribute independently as in previous releases.)
|
||||
|
||||
#### Object Protection Rules ([#10244](https://github.com/netbox-community/netbox/issues/10244))
|
||||
|
||||
A new [`PROTECTION_RULES`](../configuration/data-validation.md#protection_rules) configuration parameter is now available. Similar to how [custom validation rules](../customization/custom-validation.md) can be used to enforce certain values for object attributes, protection rules guard against the deletion of objects which do not meet specified criteria. This enables an administrator to prevent, for example, the deletion of a site which has a status of "active."
|
||||
A new [`PROTECTION_RULES`](../configuration/data-validation.md#protection_rules) configuration parameter has been introduced. Similar to how [custom validation rules](../customization/custom-validation.md) can be used to enforce certain values for object attributes, protection rules guard against the deletion of objects which do not meet specified criteria. This enables an administrator to prevent, for example, the deletion of a site which has a status of "active."
|
||||
|
||||
#### Improved Custom Field Visibility Controls ([#13299](https://github.com/netbox-community/netbox/issues/13299))
|
||||
|
||||
The old `ui_visible` field on the custom field model](../models/extras/customfield.md) has been replaced by two new fields, `ui_visible` and `ui_editable`, which control how and whether a custom field is displayed when view and editing an object, respectively. Separating these two functions into discrete fields enables more control over how each custom field is presented to users. The values of these fields will be appropriately set automatically during the upgrade process depending on the value of the original field.
|
||||
The `ui_visible` field on [the custom field model](../models/extras/customfield.md) has been superseded by two new fields, `ui_visible` and `ui_editable`, which control how and whether a custom field is displayed when view and editing an object, respectively. Separating these two functions into discrete fields allows more control over how each custom field is presented to users. The values of these fields will be appropriately set automatically during the upgrade process from the value of the original field.
|
||||
|
||||
#### Improved Global Search Results ([#14134](https://github.com/netbox-community/netbox/issues/14134))
|
||||
|
||||
@@ -50,26 +209,48 @@ Plugins can now [register their own data backends](../plugins/development/data-b
|
||||
* [#12135](https://github.com/netbox-community/netbox/issues/12135) - Avoid orphaned interfaces by preventing the deletion of interfaces which have children assigned
|
||||
* [#12216](https://github.com/netbox-community/netbox/issues/12216) - Add a `color` field for circuit types
|
||||
* [#13230](https://github.com/netbox-community/netbox/issues/13230) - Allow device types to be excluded from consideration when calculating a rack's utilization
|
||||
* [#13334](https://github.com/netbox-community/netbox/issues/13334) - Added an `error` field to the Job model to record any errors associated with its execution
|
||||
* [#13427](https://github.com/netbox-community/netbox/issues/13427) - Introduced a mechanism for omitting models from general-purpose lists of object types
|
||||
* [#13334](https://github.com/netbox-community/netbox/issues/13334) - Add an `error` field to the Job model to record any errors associated with its execution
|
||||
* [#13427](https://github.com/netbox-community/netbox/issues/13427) - Introduce a mechanism for excluding models from general-purpose lists of object types
|
||||
* [#13690](https://github.com/netbox-community/netbox/issues/13690) - Display any dependent objects to be deleted prior to deleting an object via the web UI
|
||||
* [#13794](https://github.com/netbox-community/netbox/issues/13794) - Any models with a relationship to Tenant are now included automatically in the list of related objects under the tenant view
|
||||
* [#13808](https://github.com/netbox-community/netbox/issues/13808) - Added a `/render-config` REST API endpoint for virtual machines
|
||||
* [#13808](https://github.com/netbox-community/netbox/issues/13808) - Add a `/render-config` REST API endpoint for virtual machines
|
||||
* [#14035](https://github.com/netbox-community/netbox/issues/14035) - Order objects of equivalent weight by value in global search results to improve readability
|
||||
* [#14147](https://github.com/netbox-community/netbox/issues/14147) - Avoid recording empty changelog entries via the new `CHANGELOG_SKIP_EMPTY_CHANGES` config parameter
|
||||
* [#14156](https://github.com/netbox-community/netbox/issues/14156) - Enable custom fields for contact assignments
|
||||
* [#14240](https://github.com/netbox-community/netbox/issues/14240) - Increase maximum values for custom field minimum & maximum numeric validators
|
||||
* [#14361](https://github.com/netbox-community/netbox/issues/14361) - Add a `description` field for webhooks
|
||||
* [#14365](https://github.com/netbox-community/netbox/issues/14365) - Introduced `job_start` and `job_end` signals
|
||||
* [#14365](https://github.com/netbox-community/netbox/issues/14365) - Introduce `job_start` and `job_end` signals to allow automated plugin actions
|
||||
* [#14434](https://github.com/netbox-community/netbox/issues/14434) - Add model-specific termination object filters for cables (e.g. `interface_id` and `consoleport_id`)
|
||||
* [#14436](https://github.com/netbox-community/netbox/issues/14436) - Add PostgreSQL indexes for all GenericForeignKey fields
|
||||
* [#14579](https://github.com/netbox-community/netbox/issues/14579) - Allow users to specify a preferred language for UI translations
|
||||
|
||||
### Translations
|
||||
|
||||
* [#14075](https://github.com/netbox-community/netbox/issues/14075) - Add Spanish translation
|
||||
* [#14096](https://github.com/netbox-community/netbox/issues/14096) - Add French translation
|
||||
* [#14145](https://github.com/netbox-community/netbox/issues/14145) - Add Portuguese translation
|
||||
* [#14266](https://github.com/netbox-community/netbox/issues/14266) - Add Russian translation
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14432](https://github.com/netbox-community/netbox/issues/14432) - Fix hyperlinks for global search result attributes
|
||||
* [#14472](https://github.com/netbox-community/netbox/issues/14472) - Fix display of hidden custom fields in object edit forms
|
||||
* [#14499](https://github.com/netbox-community/netbox/issues/14499) - Relax requirements for encryption/auth algorithms on IKE & IPSec proposals
|
||||
* [#14550](https://github.com/netbox-community/netbox/issues/14550) - Fix changing action type of existing event rule
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#13550](https://github.com/netbox-community/netbox/issues/13550) - Optimized the format for declaring view actions under `ActionsMixin` (backward compatibility has been retained)
|
||||
* [#13550](https://github.com/netbox-community/netbox/issues/13550) - Optimize the format for declaring view actions under `ActionsMixin` (backward compatibility has been retained)
|
||||
* [#13645](https://github.com/netbox-community/netbox/issues/13645) - Installation of the `sentry-sdk` Python library is now required only if Sentry reporting is enabled
|
||||
* [#14036](https://github.com/netbox-community/netbox/issues/14036) - Move plugin resources from the `extras` app into `netbox` (backward compatibility has been retained)
|
||||
* [#14153](https://github.com/netbox-community/netbox/issues/14153) - Replace `FeatureQuery` with new `with_feature()` method on ContentType manager
|
||||
* [#14153](https://github.com/netbox-community/netbox/issues/14153) - Replace `FeatureQuery` with new `with_feature()` method on proxy ContentType manager
|
||||
* [#14311](https://github.com/netbox-community/netbox/issues/14311) - Move the L2VPN models from the `ipam` app to the new `vpn` app
|
||||
* [#14312](https://github.com/netbox-community/netbox/issues/14312) - Move the ConfigRevision model from the `extras` app to `core`
|
||||
* [#14326](https://github.com/netbox-community/netbox/issues/14326) - Form feature mixin classes have been moved from the `extras` app to `netbox`
|
||||
* [#14395](https://github.com/netbox-community/netbox/issues/14395) - Moved `extras.webhooks_worker.process_webhook()` to `extras.webhooks.send_webhook()` (backward compatibility has been retained)
|
||||
* [#14395](https://github.com/netbox-community/netbox/issues/14395) - Move `extras.webhooks_worker.process_webhook()` to `extras.webhooks.send_webhook()` (backward compatibility has been retained)
|
||||
* [#14424](https://github.com/netbox-community/netbox/issues/14424) - Remove change logging functionality from StagedChange
|
||||
* [#14458](https://github.com/netbox-community/netbox/issues/14458) - Remove the obsolete `clearcache` management command
|
||||
* [#14536](https://github.com/netbox-community/netbox/issues/14536) - Enforce uniqueness by default for non-VRF prefixes & IP addresses (`ENFORCE_GLOBAL_UNIQUE` now defaults to true)
|
||||
|
||||
### REST API Changes
|
||||
|
||||
@@ -91,7 +272,15 @@ Plugins can now [register their own data backends](../plugins/development/data-b
|
||||
* core.Job
|
||||
* Added the read-only `error` character field
|
||||
* extras.Webhook
|
||||
* Removed the following fields: `content_types`, `type_create`, `type_update`, `type_delete`, `type_job_start`, `type_job_end`, `enabled`, and `conditions` (these have been moved to the new `EventRule` model)
|
||||
* Removed the following fields (these have been moved to the new `EventRule` model):
|
||||
* `content_types`
|
||||
* `type_create`
|
||||
* `type_update`
|
||||
* `type_delete`
|
||||
* `type_job_start`
|
||||
* `type_job_end`
|
||||
* `enabled`
|
||||
* `conditions`
|
||||
* Add the optional `description` field
|
||||
* dcim.DeviceType
|
||||
* Added the `exclude_from_utilization` boolean field
|
||||
|
||||
@@ -42,6 +42,7 @@ 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
|
||||
@@ -127,7 +128,9 @@ nav:
|
||||
- Synchronized Data: 'integrations/synchronized-data.md'
|
||||
- Prometheus Metrics: 'integrations/prometheus-metrics.md'
|
||||
- Plugins:
|
||||
- Using Plugins: 'plugins/index.md'
|
||||
- About Plugins: 'plugins/index.md'
|
||||
- Installing a Plugin: 'plugins/installation.md'
|
||||
- Removing a Plugin: 'plugins/removal.md'
|
||||
- Developing Plugins:
|
||||
- Getting Started: 'plugins/development/index.md'
|
||||
- Models: 'plugins/development/models.md'
|
||||
@@ -286,6 +289,7 @@ nav:
|
||||
- User Preferences: 'development/user-preferences.md'
|
||||
- Web UI: 'development/web-ui.md'
|
||||
- Internationalization: 'development/internationalization.md'
|
||||
- Translations: 'development/translations.md'
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.shortcuts import render, resolve_url
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic import View
|
||||
from social_core.backends.utils import load_backends
|
||||
@@ -193,8 +194,16 @@ class UserConfigView(LoginRequiredMixin, View):
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
messages.success(request, "Your preferences have been updated.")
|
||||
return redirect('account:preferences')
|
||||
messages.success(request, _("Your preferences have been updated."))
|
||||
response = redirect('account:preferences')
|
||||
|
||||
# Set/clear language cookie
|
||||
if language := form.cleaned_data['locale.language']:
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
||||
else:
|
||||
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
|
||||
|
||||
return response
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
|
||||
@@ -64,16 +64,23 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('ASN (ID)'),
|
||||
)
|
||||
asn = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='asns__asn',
|
||||
queryset=ASN.objects.all(),
|
||||
to_field_name='asn',
|
||||
label=_('ASN'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug']
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(accounts__account__icontains=value) |
|
||||
Q(accounts__name__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
@@ -101,6 +108,7 @@ class ProviderAccountFilterSet(NetBoxModelFilterSet):
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(account__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
@@ -24,7 +24,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Location'), ('region_id', 'site_group_id', 'site_id')),
|
||||
(_('ASN'), ('asn',)),
|
||||
(_('ASN'), ('asn_id',)),
|
||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
@@ -46,10 +46,6 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('ASN (legacy)')
|
||||
)
|
||||
asn_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
required=False,
|
||||
@@ -119,6 +115,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False,
|
||||
|
||||
@@ -234,9 +234,9 @@ class CircuitTermination(
|
||||
|
||||
# Must define either site *or* provider network
|
||||
if self.site is None and self.provider_network is None:
|
||||
raise ValidationError("A circuit termination must attach to either a site or a provider network.")
|
||||
raise ValidationError(_("A circuit termination must attach to either a site or a provider network."))
|
||||
if self.site and self.provider_network:
|
||||
raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
|
||||
raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network."))
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
|
||||
@@ -25,8 +25,8 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 1', slug='provider-1', description='foobar1'),
|
||||
Provider(name='Provider 2', slug='provider-2', description='foobar2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
Provider(name='Provider 4', slug='provider-4'),
|
||||
Provider(name='Provider 5', slug='provider-5'),
|
||||
@@ -74,6 +74,10 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider 1', 'Provider 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -82,10 +86,16 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'slug': ['provider-1', 'provider-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asn_id(self): # ASN object assignment
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asn(self):
|
||||
asns = ASN.objects.all()[:2]
|
||||
params = {'asn_id': [asns[0].pk, asns[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'asn': [asns[0].asn, asns[1].asn]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
@@ -122,6 +132,10 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Circuit Type 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -227,6 +241,10 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_cid(self):
|
||||
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -369,6 +387,10 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
Cable(a_terminations=[circuit_terminations[0]], b_terminations=[circuit_terminations[1]]).save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_term_side(self):
|
||||
params = {'term_side': 'A'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
@@ -440,6 +462,10 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider Network 1', 'Provider Network 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -477,6 +503,10 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider Account 1', 'Provider Account 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -8,6 +8,7 @@ from drf_spectacular.plumbing import (
|
||||
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
from rest_framework.relations import ManyRelatedField
|
||||
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
|
||||
@@ -36,7 +36,7 @@ class DataSourceSerializer(NetBoxModelSerializer):
|
||||
model = DataSource
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
|
||||
'parameters', 'ignore_rules', 'created', 'last_updated', 'file_count',
|
||||
'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
@@ -33,10 +33,11 @@ class DataSourceViewSet(NetBoxModelViewSet):
|
||||
"""
|
||||
Enqueue a job to synchronize the DataSource.
|
||||
"""
|
||||
if not request.user.has_perm('core.sync_datasource'):
|
||||
raise PermissionDenied("Syncing data sources requires the core.sync_datasource permission.")
|
||||
|
||||
datasource = get_object_or_404(DataSource, pk=pk)
|
||||
|
||||
if not request.user.has_perm('core.sync_datasource', obj=datasource):
|
||||
raise PermissionDenied(_("This user does not have permission to synchronize this data source."))
|
||||
|
||||
datasource.enqueue_sync_job(request)
|
||||
serializer = serializers.DataSourceSerializer(datasource, context={'request': request})
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class GitBackend(DataBackend):
|
||||
try:
|
||||
porcelain.clone(self.url, local_path.name, **clone_args)
|
||||
except BaseException as e:
|
||||
raise SyncError(f"Fetching remote data failed ({type(e).__name__}): {e}")
|
||||
raise SyncError(_("Fetching remote data failed ({name}): {error}").format(name=type(e).__name__, error=e))
|
||||
|
||||
yield local_path.name
|
||||
|
||||
@@ -149,7 +149,8 @@ class S3Backend(DataBackend):
|
||||
region_name=self._region_name,
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
config=self.config
|
||||
config=self.config,
|
||||
endpoint_url=self._endpoint_url
|
||||
)
|
||||
bucket = s3.Bucket(self._bucket_name)
|
||||
|
||||
@@ -176,6 +177,11 @@ class S3Backend(DataBackend):
|
||||
url_path = urlparse(self.url).path.lstrip('/')
|
||||
return url_path.split('/')[0]
|
||||
|
||||
@property
|
||||
def _endpoint_url(self):
|
||||
url_path = urlparse(self.url)
|
||||
return url_path._replace(params="", fragment="", query="", path="").geturl()
|
||||
|
||||
@property
|
||||
def _remote_path(self):
|
||||
url_path = urlparse(self.url).path.lstrip('/')
|
||||
|
||||
@@ -28,7 +28,7 @@ class DataSourceFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = ('id', 'name', 'enabled')
|
||||
fields = ('id', 'name', 'enabled', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -21,7 +21,7 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect(),
|
||||
label=_('Enforce unique space')
|
||||
label=_('Enabled')
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.forms.fields import JSONField as _JSONField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.forms.mixins import SyncedDataMixin
|
||||
@@ -12,7 +13,7 @@ from netbox.forms import NetBoxModelForm
|
||||
from netbox.registry import registry
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms import BootstrapMixin, get_field_value
|
||||
from utilities.forms.fields import CommentField
|
||||
from utilities.forms.fields import CommentField, JSONField
|
||||
from utilities.forms.widgets import HTMXSelect
|
||||
|
||||
__all__ = (
|
||||
@@ -103,9 +104,9 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
|
||||
super().clean()
|
||||
|
||||
if self.cleaned_data.get('upload_file') and self.cleaned_data.get('data_file'):
|
||||
raise forms.ValidationError("Cannot upload a file and sync from an existing file")
|
||||
raise forms.ValidationError(_("Cannot upload a file and sync from an existing file"))
|
||||
if not self.cleaned_data.get('upload_file') and not self.cleaned_data.get('data_file'):
|
||||
raise forms.ValidationError("Must upload a file or select a data file to sync")
|
||||
raise forms.ValidationError(_("Must upload a file or select a data file to sync"))
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
@@ -132,6 +133,9 @@ class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
|
||||
'help_text': param.description,
|
||||
}
|
||||
field_kwargs.update(**param.field_kwargs)
|
||||
if param.field is _JSONField:
|
||||
# Replace with our own JSONField to get pretty JSON in config editor
|
||||
param.field = JSONField
|
||||
param_fields[param.name] = param.field(**field_kwargs)
|
||||
attrs.update(param_fields)
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from django.core.cache import cache
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.models import ConfigRevision
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Command to clear the entire cache."""
|
||||
help = 'Clears the cache.'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Fetch the current config revision from the cache
|
||||
config_version = cache.get('config_version')
|
||||
# Clear the cache
|
||||
cache.clear()
|
||||
self.stdout.write('Cache has been cleared.', ending="\n")
|
||||
if config_version:
|
||||
# Activate the current config revision
|
||||
ConfigRevision.objects.get(id=config_version).activate()
|
||||
self.stdout.write(f'Config revision ({config_version}) has been restored.', ending="\n")
|
||||
@@ -9,9 +9,9 @@ class Command(_Command):
|
||||
"""
|
||||
This built-in management command enables the creation of new database schema migration files, which should
|
||||
never be required by and ordinary user. We prevent this command from executing unless the configuration
|
||||
indicates that the user is a developer (i.e. configuration.DEVELOPER == True).
|
||||
indicates that the user is a developer (i.e. configuration.DEVELOPER == True), or it was run with --check.
|
||||
"""
|
||||
if not settings.DEVELOPER:
|
||||
if not kwargs['check_changes'] and not settings.DEVELOPER:
|
||||
raise CommandError(
|
||||
"This command is available for development purposes only. It will\n"
|
||||
"NOT resolve any issues with missing or unapplied migrations. For assistance,\n"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from core.choices import DataSourceStatusChoices
|
||||
from core.models import DataSource
|
||||
|
||||
|
||||
@@ -33,9 +34,13 @@ class Command(BaseCommand):
|
||||
for i, datasource in enumerate(datasources, start=1):
|
||||
self.stdout.write(f"[{i}] Syncing {datasource}... ", ending='')
|
||||
self.stdout.flush()
|
||||
datasource.sync()
|
||||
self.stdout.write(datasource.get_status_display())
|
||||
self.stdout.flush()
|
||||
try:
|
||||
datasource.sync()
|
||||
self.stdout.write(datasource.get_status_display())
|
||||
self.stdout.flush()
|
||||
except Exception as e:
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
||||
raise e
|
||||
|
||||
if len(options['name']) > 1:
|
||||
self.stdout.write(f"Finished.")
|
||||
|
||||
17
netbox/core/migrations/0010_gfk_indexes.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-07 16:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_configrevision'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='job',
|
||||
index=models.Index(fields=['object_type', 'object_id'], name='core_job_object__c664ac_idx'),
|
||||
),
|
||||
]
|
||||
@@ -44,7 +44,7 @@ class ConfigRevision(models.Model):
|
||||
return gettext('Config revision #{id}').format(id=self.pk)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in self.data:
|
||||
if self.data and item in self.data:
|
||||
return self.data[item]
|
||||
return super().__getattribute__(item)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.utils import timezone
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
from netbox.models import PrimaryModel
|
||||
from netbox.models.features import JobsMixin
|
||||
from netbox.registry import registry
|
||||
@@ -130,6 +131,28 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
'source_url': f"URLs for local sources must start with file:// (or specify no scheme)"
|
||||
})
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
|
||||
# Censor any backend parameters marked as sensitive in the serialized data
|
||||
pre_change_params = {}
|
||||
post_change_params = {}
|
||||
if objectchange.prechange_data:
|
||||
pre_change_params = objectchange.prechange_data.get('parameters') or {} # parameters may be None
|
||||
if objectchange.postchange_data:
|
||||
post_change_params = objectchange.postchange_data.get('parameters') or {}
|
||||
for param in self.backend_class.sensitive_parameters:
|
||||
if post_change_params.get(param):
|
||||
if post_change_params[param] != pre_change_params.get(param):
|
||||
# Set the "changed" token if the parameter's value has been modified
|
||||
post_change_params[param] = CENSOR_TOKEN_CHANGED
|
||||
else:
|
||||
post_change_params[param] = CENSOR_TOKEN
|
||||
if pre_change_params.get(param):
|
||||
pre_change_params[param] = CENSOR_TOKEN
|
||||
|
||||
return objectchange
|
||||
|
||||
def enqueue_sync_job(self, request):
|
||||
"""
|
||||
Enqueue a background job to synchronize the DataSource by calling sync().
|
||||
@@ -154,7 +177,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
|
||||
"""
|
||||
if self.status == DataSourceStatusChoices.SYNCING:
|
||||
raise SyncError("Cannot initiate sync; syncing already in progress.")
|
||||
raise SyncError(_("Cannot initiate sync; syncing already in progress."))
|
||||
|
||||
# Emit the pre_sync signal
|
||||
pre_sync.send(sender=self.__class__, instance=self)
|
||||
@@ -167,7 +190,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
backend = self.get_backend()
|
||||
except ModuleNotFoundError as e:
|
||||
raise SyncError(
|
||||
f"There was an error initializing the backend. A dependency needs to be installed: {e}"
|
||||
_("There was an error initializing the backend. A dependency needs to be installed: ") + str(e)
|
||||
)
|
||||
with backend.fetch() as local_path:
|
||||
|
||||
|
||||
@@ -106,6 +106,9 @@ class Job(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created']
|
||||
indexes = (
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
verbose_name = _('job')
|
||||
verbose_name_plural = _('jobs')
|
||||
|
||||
@@ -178,7 +181,11 @@ class Job(models.Model):
|
||||
"""
|
||||
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
|
||||
if status not in valid_statuses:
|
||||
raise ValueError(f"Invalid status for job termination. Choices are: {', '.join(valid_statuses)}")
|
||||
raise ValueError(
|
||||
_("Invalid status for job termination. Choices are: {choices}").format(
|
||||
choices=', '.join(valid_statuses)
|
||||
)
|
||||
)
|
||||
|
||||
# Mark the job as completed
|
||||
self.status = status
|
||||
|
||||
@@ -20,3 +20,4 @@ class DataFileIndex(SearchIndex):
|
||||
fields = (
|
||||
('path', 200),
|
||||
)
|
||||
display_attrs = ('source',)
|
||||
|
||||
@@ -21,14 +21,16 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
type='local',
|
||||
source_url='file:///var/tmp/source1/',
|
||||
status=DataSourceStatusChoices.NEW,
|
||||
enabled=True
|
||||
enabled=True,
|
||||
description='foobar1'
|
||||
),
|
||||
DataSource(
|
||||
name='Data Source 2',
|
||||
type='local',
|
||||
source_url='file:///var/tmp/source2/',
|
||||
status=DataSourceStatusChoices.SYNCING,
|
||||
enabled=True
|
||||
enabled=True,
|
||||
description='foobar2'
|
||||
),
|
||||
DataSource(
|
||||
name='Data Source 3',
|
||||
@@ -40,10 +42,18 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
DataSource.objects.bulk_create(data_sources)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Data Source 1', 'Data Source 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
params = {'type': ['local']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -97,6 +107,10 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
DataFile.objects.bulk_create(data_files)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'file1.txt'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_source(self):
|
||||
sources = DataSource.objects.all()
|
||||
params = {'source_id': [sources[0].pk, sources[1].pk]}
|
||||
|
||||
122
netbox/core/tests/test_models.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import DataSource
|
||||
from extras.choices import ObjectChangeActionChoices
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
|
||||
|
||||
class DataSourceChangeLoggingTestCase(TestCase):
|
||||
|
||||
def test_password_added_on_create(self):
|
||||
datasource = DataSource.objects.create(
|
||||
name='Data Source 1',
|
||||
type='git',
|
||||
source_url='http://localhost/',
|
||||
parameters={
|
||||
'username': 'jeff',
|
||||
'password': 'foobar123',
|
||||
}
|
||||
)
|
||||
|
||||
objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertIsNone(objectchange.prechange_data)
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff')
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN_CHANGED)
|
||||
|
||||
def test_password_added_on_update(self):
|
||||
datasource = DataSource.objects.create(
|
||||
name='Data Source 1',
|
||||
type='git',
|
||||
source_url='http://localhost/'
|
||||
)
|
||||
datasource.snapshot()
|
||||
|
||||
# Add a blank password
|
||||
datasource.parameters = {
|
||||
'username': 'jeff',
|
||||
'password': '',
|
||||
}
|
||||
|
||||
objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertIsNone(objectchange.prechange_data['parameters'])
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff')
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['password'], '')
|
||||
|
||||
# Add a password
|
||||
datasource.parameters = {
|
||||
'username': 'jeff',
|
||||
'password': 'foobar123',
|
||||
}
|
||||
|
||||
objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff')
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN_CHANGED)
|
||||
|
||||
def test_password_changed(self):
|
||||
datasource = DataSource.objects.create(
|
||||
name='Data Source 1',
|
||||
type='git',
|
||||
source_url='http://localhost/',
|
||||
parameters={
|
||||
'username': 'jeff',
|
||||
'password': 'password1',
|
||||
}
|
||||
)
|
||||
datasource.snapshot()
|
||||
|
||||
# Change the password
|
||||
datasource.parameters['password'] = 'password2'
|
||||
|
||||
objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(objectchange.prechange_data['parameters']['username'], 'jeff')
|
||||
self.assertEqual(objectchange.prechange_data['parameters']['password'], CENSOR_TOKEN)
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff')
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN_CHANGED)
|
||||
|
||||
def test_password_removed_on_update(self):
|
||||
datasource = DataSource.objects.create(
|
||||
name='Data Source 1',
|
||||
type='git',
|
||||
source_url='http://localhost/',
|
||||
parameters={
|
||||
'username': 'jeff',
|
||||
'password': 'foobar123',
|
||||
}
|
||||
)
|
||||
datasource.snapshot()
|
||||
|
||||
objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(objectchange.prechange_data['parameters']['username'], 'jeff')
|
||||
self.assertEqual(objectchange.prechange_data['parameters']['password'], CENSOR_TOKEN)
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff')
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN)
|
||||
|
||||
# Remove the password
|
||||
datasource.parameters['password'] = ''
|
||||
|
||||
objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(objectchange.prechange_data['parameters']['username'], 'jeff')
|
||||
self.assertEqual(objectchange.prechange_data['parameters']['password'], CENSOR_TOKEN)
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['username'], 'jeff')
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['password'], '')
|
||||
|
||||
def test_password_not_modified(self):
|
||||
datasource = DataSource.objects.create(
|
||||
name='Data Source 1',
|
||||
type='git',
|
||||
source_url='http://localhost/',
|
||||
parameters={
|
||||
'username': 'username1',
|
||||
'password': 'foobar123',
|
||||
}
|
||||
)
|
||||
datasource.snapshot()
|
||||
|
||||
# Remove the password
|
||||
datasource.parameters['username'] = 'username2'
|
||||
|
||||
objectchange = datasource.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(objectchange.prechange_data['parameters']['username'], 'username1')
|
||||
self.assertEqual(objectchange.prechange_data['parameters']['password'], CENSOR_TOKEN)
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['username'], 'username2')
|
||||
self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN)
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.views.generic import View
|
||||
@@ -159,12 +160,14 @@ class ConfigView(generic.ObjectView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
if config := self.queryset.first():
|
||||
return config
|
||||
# Instantiate a dummy default config if none has been created yet
|
||||
return ConfigRevision(
|
||||
data=get_config().defaults
|
||||
)
|
||||
revision_id = cache.get('config_version')
|
||||
try:
|
||||
return ConfigRevision.objects.get(pk=revision_id)
|
||||
except ConfigRevision.DoesNotExist:
|
||||
# Fall back to using the active config data if no record is found
|
||||
return ConfigRevision(
|
||||
data=get_config().defaults
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionListView(generic.ObjectListView):
|
||||
|
||||
@@ -414,11 +414,11 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class NestedModuleBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||
module = NestedModuleSerializer(required=False, read_only=True, allow_null=True)
|
||||
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBay
|
||||
fields = ['id', 'url', 'display', 'module', 'name']
|
||||
fields = ['id', 'url', 'display', 'installed_module', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
|
||||
@@ -326,6 +326,8 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
front_image = serializers.ImageField(required=False, allow_null=True)
|
||||
rear_image = serializers.ImageField(required=False, allow_null=True)
|
||||
|
||||
# Counter fields
|
||||
console_port_template_count = serializers.IntegerField(read_only=True)
|
||||
@@ -610,7 +612,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
component = serializers.SerializerMethodField(read_only=True)
|
||||
component = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -666,7 +668,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
role = NestedDeviceRoleSerializer()
|
||||
device_role = NestedDeviceRoleSerializer(read_only=True, help_text='Deprecated in v3.6 in favor of `role`.')
|
||||
device_role = NestedDeviceRoleSerializer(read_only=True, help_text=_('Deprecated in v3.6 in favor of `role`.'))
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer()
|
||||
@@ -683,7 +685,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
)
|
||||
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
oob_ip = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
@@ -733,7 +735,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
|
||||
|
||||
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
config_context = serializers.SerializerMethodField(read_only=True)
|
||||
config_context = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
@@ -1037,8 +1039,7 @@ class ModuleBaySerializer(NetBoxModelSerializer):
|
||||
model = ModuleBay
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags',
|
||||
'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -1066,7 +1067,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
component = serializers.SerializerMethodField(read_only=True)
|
||||
component = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -191,6 +191,12 @@ class RackViewSet(NetBoxModelViewSet):
|
||||
serializer_class = serializers.RackSerializer
|
||||
filterset_class = filtersets.RackFilterSet
|
||||
|
||||
@extend_schema(
|
||||
operation_id='dcim_racks_elevation_retrieve',
|
||||
filters=False,
|
||||
parameters=[serializers.RackElevationDetailFilterSerializer],
|
||||
responses={200: serializers.RackUnitSerializer(many=True)}
|
||||
)
|
||||
@action(detail=True)
|
||||
def elevation(self, request, pk=None):
|
||||
"""
|
||||
|
||||
@@ -889,7 +889,10 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
|
||||
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
||||
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
||||
TYPE_32GFC_SFP_PLUS = '32gfc-sfpp'
|
||||
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
|
||||
TYPE_64GFC_SFP_DD = '64gfc-sfpdd'
|
||||
TYPE_64GFC_SFP_PLUS = '64gfc-sfpp'
|
||||
TYPE_128GFC_QSFP28 = '128gfc-qsfp28'
|
||||
|
||||
# InfiniBand
|
||||
@@ -1058,7 +1061,10 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
|
||||
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
|
||||
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
|
||||
(TYPE_32GFC_SFP_PLUS, 'SFP+ (32GFC)'),
|
||||
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
|
||||
(TYPE_64GFC_SFP_DD, 'SFP-DD (64GFC)'),
|
||||
(TYPE_64GFC_SFP_PLUS, 'SFP+ (64GFC)'),
|
||||
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
|
||||
)
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
|
||||
|
||||
from .lookups import PathContains
|
||||
@@ -41,7 +42,7 @@ class MACAddressField(models.Field):
|
||||
try:
|
||||
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
|
||||
except AddrFormatError:
|
||||
raise ValidationError(f"Invalid MAC address format: {value}")
|
||||
raise ValidationError(_("Invalid MAC address format: {value}").format(value=value))
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'macaddr'
|
||||
@@ -67,7 +68,7 @@ class WWNField(models.Field):
|
||||
try:
|
||||
return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase)
|
||||
except AddrFormatError:
|
||||
raise ValidationError(f"Invalid WWN format: {value}")
|
||||
raise ValidationError(_("Invalid WWN format: {value}").format(value=value))
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'macaddr8'
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import django_filters
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.filtersets import PrimaryIPFilterSet
|
||||
@@ -326,7 +330,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit'
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -337,6 +341,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
Q(facility_id__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@@ -498,8 +503,8 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role',
|
||||
'airflow', 'weight', 'weight_unit',
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
|
||||
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'description',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -509,6 +514,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
Q(manufacturer__name__icontains=value) |
|
||||
Q(model__icontains=value) |
|
||||
Q(part_number__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@@ -593,7 +599,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = ['id', 'model', 'part_number', 'weight', 'weight_unit']
|
||||
fields = ['id', 'model', 'part_number', 'weight', 'weight_unit', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -602,6 +608,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||
Q(manufacturer__name__icontains=value) |
|
||||
Q(model__icontains=value) |
|
||||
Q(part_number__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@@ -641,7 +648,10 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(name__icontains=value)
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
|
||||
@@ -656,21 +666,21 @@ class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
fields = ['id', 'name', 'type', 'description']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
fields = ['id', 'name', 'type', 'description']
|
||||
|
||||
|
||||
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||
|
||||
|
||||
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@@ -681,7 +691,7 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'name', 'type', 'feed_leg']
|
||||
fields = ['id', 'name', 'type', 'feed_leg', 'description']
|
||||
|
||||
|
||||
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@@ -705,7 +715,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'name', 'type', 'enabled', 'mgmt_only']
|
||||
fields = ['id', 'name', 'type', 'enabled', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@@ -716,7 +726,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'name', 'type', 'color']
|
||||
fields = ['id', 'name', 'type', 'color', 'description']
|
||||
|
||||
|
||||
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@@ -727,21 +737,21 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'name', 'type', 'color', 'positions']
|
||||
fields = ['id', 'name', 'type', 'color', 'positions', 'description']
|
||||
|
||||
|
||||
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleBayTemplate
|
||||
fields = ['id', 'name']
|
||||
fields = ['id', 'name', 'description']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'name']
|
||||
fields = ['id', 'name', 'description']
|
||||
|
||||
|
||||
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
@@ -774,7 +784,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemTemplate
|
||||
fields = ['id', 'name', 'label', 'part_id']
|
||||
fields = ['id', 'name', 'label', 'part_id', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -810,6 +820,10 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label=_('Manufacturer (slug)'),
|
||||
)
|
||||
available_for_device_type = django_filters.ModelChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
method='get_for_device_type'
|
||||
)
|
||||
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
label=_('Config template (ID)'),
|
||||
@@ -819,6 +833,14 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_for_device_type(self, queryset, name, value):
|
||||
"""
|
||||
Return all Platforms available for a specific manufacturer based on device type and Platforms not assigned any
|
||||
manufacturer
|
||||
"""
|
||||
return queryset.filter(Q(manufacturer=None) | Q(manufacturer__device_types=value))
|
||||
|
||||
|
||||
class DeviceFilterSet(
|
||||
NetBoxModelFilterSet,
|
||||
@@ -1010,7 +1032,10 @@ class DeviceFilterSet(
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority']
|
||||
fields = [
|
||||
'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority',
|
||||
'description',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1020,6 +1045,7 @@ class DeviceFilterSet(
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(inventoryitems__serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
Q(description__icontains=value.strip()) |
|
||||
Q(comments__icontains=value) |
|
||||
Q(primary_ip4__address__startswith=value) |
|
||||
Q(primary_ip6__address__startswith=value)
|
||||
@@ -1089,13 +1115,16 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, Prim
|
||||
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
fields = ['id', 'device', 'name']
|
||||
fields = ['id', 'device', 'name', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
|
||||
qs_filter = Q(name__icontains=value)
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
try:
|
||||
qs_filter |= Q(identifier=int(value))
|
||||
except ValueError:
|
||||
@@ -1152,7 +1181,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = ['id', 'status', 'asset_tag']
|
||||
fields = ['id', 'status', 'asset_tag', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1161,6 +1190,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
||||
Q(device__name__icontains=value.strip()) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
@@ -1651,7 +1681,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||
|
||||
|
||||
class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||
@@ -1716,13 +1746,14 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'domain', 'name']
|
||||
fields = ['id', 'domain', 'name', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(members__name__icontains=value) |
|
||||
Q(domain__icontains=value)
|
||||
)
|
||||
@@ -1789,14 +1820,47 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
field_name='site__slug'
|
||||
)
|
||||
|
||||
# Termination object filters
|
||||
consoleport_id = MultiValueNumberFilter(
|
||||
method='filter_by_consoleport'
|
||||
)
|
||||
consoleserverport_id = MultiValueNumberFilter(
|
||||
method='filter_by_consoleserverport'
|
||||
)
|
||||
powerport_id = MultiValueNumberFilter(
|
||||
method='filter_by_powerport'
|
||||
)
|
||||
poweroutlet_id = MultiValueNumberFilter(
|
||||
method='filter_by_poweroutlet'
|
||||
)
|
||||
interface_id = MultiValueNumberFilter(
|
||||
method='filter_by_interface'
|
||||
)
|
||||
frontport_id = MultiValueNumberFilter(
|
||||
method='filter_by_frontport'
|
||||
)
|
||||
rearport_id = MultiValueNumberFilter(
|
||||
method='filter_by_rearport'
|
||||
)
|
||||
powerfeed_id = MultiValueNumberFilter(
|
||||
method='filter_by_powerfeed'
|
||||
)
|
||||
circuittermination_id = MultiValueNumberFilter(
|
||||
method='filter_by_circuittermination'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'label', 'length', 'length_unit']
|
||||
fields = ['id', 'label', 'length', 'length_unit', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(label__icontains=value)
|
||||
qs_filter = (
|
||||
Q(label__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_by_termination(self, queryset, name, value):
|
||||
# Filter by a related object cached on CableTermination. Note the underscore preceding the field name.
|
||||
@@ -1828,6 +1892,42 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
terminations__cable_end=CableEndChoices.SIDE_B
|
||||
)
|
||||
|
||||
def filter_by_termination_object(self, queryset, model, value):
|
||||
# Filter by specific termination object(s)
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
cable_ids = CableTermination.objects.filter(
|
||||
termination_type=content_type,
|
||||
termination_id__in=value
|
||||
).values_list('cable', flat=True)
|
||||
return queryset.filter(pk__in=cable_ids)
|
||||
|
||||
def filter_by_consoleport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, ConsolePort, value)
|
||||
|
||||
def filter_by_consoleserverport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, ConsoleServerPort, value)
|
||||
|
||||
def filter_by_powerport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, PowerPort, value)
|
||||
|
||||
def filter_by_poweroutlet(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, PowerOutlet, value)
|
||||
|
||||
def filter_by_interface(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, Interface, value)
|
||||
|
||||
def filter_by_frontport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, FrontPort, value)
|
||||
|
||||
def filter_by_rearport(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, RearPort, value)
|
||||
|
||||
def filter_by_powerfeed(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, PowerFeed, value)
|
||||
|
||||
def filter_by_circuittermination(self, queryset, name, value):
|
||||
return self.filter_by_termination_object(queryset, CircuitTermination, value)
|
||||
|
||||
|
||||
class CableTerminationFilterSet(BaseFilterSet):
|
||||
termination_type = ContentTypeFilter()
|
||||
@@ -1883,13 +1983,14 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['id', 'name']
|
||||
fields = ['id', 'name', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value)
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -1950,6 +2051,7 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'cable_end',
|
||||
'description',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -1957,6 +2059,7 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(power_panel__name__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
@@ -412,7 +412,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
u_height = forms.IntegerField(
|
||||
label=_('U height'),
|
||||
min_value=1,
|
||||
min_value=0,
|
||||
required=False
|
||||
)
|
||||
is_full_depth = forms.NullBooleanField(
|
||||
|
||||
@@ -159,6 +159,14 @@ class LocationImportForm(NetBoxModelImportForm):
|
||||
model = Location
|
||||
fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description', 'tags')
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
# Limit location queryset by assigned site
|
||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
||||
|
||||
|
||||
class RackRoleImportForm(NetBoxModelImportForm):
|
||||
slug = SlugField()
|
||||
@@ -727,7 +735,7 @@ class PowerOutletImportForm(NetBoxModelImportForm):
|
||||
help_text=_('Local power port which feeds this outlet')
|
||||
)
|
||||
feed_leg = CSVChoiceField(
|
||||
label=_('Feed lag'),
|
||||
label=_('Feed leg'),
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
required=False,
|
||||
help_text=_('Electrical phase (for three-phase circuits)')
|
||||
@@ -870,7 +878,11 @@ class InterfaceImportForm(NetBoxModelImportForm):
|
||||
def clean_vdcs(self):
|
||||
for vdc in self.cleaned_data['vdcs']:
|
||||
if vdc.device != self.cleaned_data['device']:
|
||||
raise forms.ValidationError(f"VDC {vdc} is not assigned to device {self.cleaned_data['device']}")
|
||||
raise forms.ValidationError(
|
||||
_("VDC {vdc} is not assigned to device {device}").format(
|
||||
vdc=vdc, device=self.cleaned_data['device']
|
||||
)
|
||||
)
|
||||
return self.cleaned_data['vdcs']
|
||||
|
||||
|
||||
@@ -996,7 +1008,7 @@ class DeviceBayImportForm(NetBoxModelImportForm):
|
||||
device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
|
||||
).exclude(pk=device.pk)
|
||||
else:
|
||||
self.fields['installed_device'].queryset = Interface.objects.none()
|
||||
self.fields['installed_device'].queryset = Device.objects.none()
|
||||
|
||||
|
||||
class InventoryItemImportForm(NetBoxModelImportForm):
|
||||
@@ -1075,7 +1087,11 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
||||
component = model.objects.get(device=device, name=component_name)
|
||||
self.instance.component = component
|
||||
except ObjectDoesNotExist:
|
||||
raise forms.ValidationError(f"Component not found: {device} - {component_name}")
|
||||
raise forms.ValidationError(
|
||||
_("Component not found: {device} - {component_name}").format(
|
||||
device=device, component_name=component_name
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -1193,10 +1209,17 @@ class CableImportForm(NetBoxModelImportForm):
|
||||
else:
|
||||
termination_object = model.objects.get(device=device, name=name)
|
||||
if termination_object.cable is not None and termination_object.cable != self.instance:
|
||||
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
|
||||
raise forms.ValidationError(
|
||||
_("Side {side_upper}: {device} {termination_object} is already connected").format(
|
||||
side_upper=side.upper(), device=device, termination_object=termination_object
|
||||
)
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
|
||||
|
||||
raise forms.ValidationError(
|
||||
_("{side_upper} side termination not found: {device} {name}").format(
|
||||
side_upper=side.upper(), device=device, name=name
|
||||
)
|
||||
)
|
||||
setattr(self.instance, f'{side}_terminations', [termination_object])
|
||||
return termination_object
|
||||
|
||||
@@ -1350,14 +1373,18 @@ class VirtualDeviceContextImportForm(NetBoxModelImportForm):
|
||||
label=_('Device'),
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned role'
|
||||
help_text=_('Assigned role')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
label=_('Status'),
|
||||
choices=VirtualDeviceContextStatusChoices,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -165,6 +165,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
|
||||
status = forms.MultipleChoiceField(
|
||||
label=_('Status'),
|
||||
choices=SiteStatusChoices,
|
||||
@@ -248,6 +249,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||
(_('Weight'), ('weight', 'max_weight', 'weight_unit')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -420,6 +422,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
)),
|
||||
(_('Weight'), ('weight', 'weight_unit')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
@@ -544,6 +547,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
)),
|
||||
(_('Weight'), ('weight', 'weight_unit')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
@@ -620,6 +624,7 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
class PlatformFilterForm(NetBoxModelFilterSetForm):
|
||||
model = Platform
|
||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
@@ -654,6 +659,7 @@ class DeviceFilterForm(
|
||||
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
|
||||
))
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -971,9 +977,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
length = forms.IntegerField(
|
||||
length = forms.DecimalField(
|
||||
label=_('Length'),
|
||||
required=False
|
||||
required=False,
|
||||
)
|
||||
length_unit = forms.ChoiceField(
|
||||
label=_('Length unit'),
|
||||
@@ -997,6 +1003,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -1228,6 +1235,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
|
||||
(_('Connection'), ('cabled', 'connected', 'occupied')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'device_id')
|
||||
vdc_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
required=False,
|
||||
|
||||
@@ -291,7 +291,11 @@ class DeviceTypeForm(NetBoxModelForm):
|
||||
default_platform = DynamicModelChoiceField(
|
||||
label=_('Default platform'),
|
||||
queryset=Platform.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
selector=True,
|
||||
query_params={
|
||||
'manufacturer_id': ['$manufacturer', 'null'],
|
||||
}
|
||||
)
|
||||
slug = SlugField(
|
||||
label=_('Slug'),
|
||||
@@ -444,7 +448,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
label=_('Platform'),
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
selector=True
|
||||
selector=True,
|
||||
query_params={
|
||||
'available_for_device_type': '$device_type',
|
||||
}
|
||||
)
|
||||
cluster = DynamicModelChoiceField(
|
||||
label=_('Cluster'),
|
||||
@@ -969,21 +976,67 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
component_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS,
|
||||
# Assigned component selectors
|
||||
consoleporttemplate = DynamicModelChoiceField(
|
||||
queryset=ConsolePortTemplate.objects.all(),
|
||||
required=False,
|
||||
widget=forms.HiddenInput
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Console port template')
|
||||
)
|
||||
component_id = forms.IntegerField(
|
||||
consoleserverporttemplate = DynamicModelChoiceField(
|
||||
queryset=ConsoleServerPortTemplate.objects.all(),
|
||||
required=False,
|
||||
widget=forms.HiddenInput
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Console server port template')
|
||||
)
|
||||
frontporttemplate = DynamicModelChoiceField(
|
||||
queryset=FrontPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Front port template')
|
||||
)
|
||||
interfacetemplate = DynamicModelChoiceField(
|
||||
queryset=InterfaceTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Interface template')
|
||||
)
|
||||
poweroutlettemplate = DynamicModelChoiceField(
|
||||
queryset=PowerOutletTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Power outlet template')
|
||||
)
|
||||
powerporttemplate = DynamicModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Power port template')
|
||||
)
|
||||
rearporttemplate = DynamicModelChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Rear port template')
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
(None, (
|
||||
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
|
||||
'component_type', 'component_id',
|
||||
)),
|
||||
)
|
||||
|
||||
@@ -991,9 +1044,52 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
|
||||
model = InventoryItemTemplate
|
||||
fields = [
|
||||
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
|
||||
'component_type', 'component_id',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
component_type = initial.get('component_type')
|
||||
component_id = initial.get('component_id')
|
||||
|
||||
# Used for picking the default active tab for component selection
|
||||
self.no_component = True
|
||||
|
||||
if instance:
|
||||
# When editing set the initial value for component selection
|
||||
for component_model in ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS):
|
||||
if type(instance.component) is component_model.model_class():
|
||||
initial[component_model.model] = instance.component
|
||||
self.no_component = False
|
||||
break
|
||||
elif component_type and component_id:
|
||||
# When adding the InventoryItem from a component page
|
||||
if content_type := ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS).filter(pk=component_type).first():
|
||||
if component := content_type.model_class().objects.filter(pk=component_id).first():
|
||||
initial[content_type.model] = component
|
||||
self.no_component = False
|
||||
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Handle object assignment
|
||||
selected_objects = [
|
||||
field for field in (
|
||||
'consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate',
|
||||
'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate'
|
||||
) if self.cleaned_data[field]
|
||||
]
|
||||
if len(selected_objects) > 1:
|
||||
raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
|
||||
elif selected_objects:
|
||||
self.instance.component = self.cleaned_data[selected_objects[0]]
|
||||
else:
|
||||
self.instance.component = None
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import graphene
|
||||
from circuits.graphql.types import CircuitTerminationType
|
||||
from circuits.models import CircuitTermination
|
||||
from circuits.graphql.types import CircuitTerminationType, ProviderNetworkType
|
||||
from circuits.models import CircuitTermination, ProviderNetwork
|
||||
from dcim.graphql.types import (
|
||||
ConsolePortTemplateType,
|
||||
ConsolePortType,
|
||||
@@ -167,3 +167,42 @@ class InventoryItemComponentType(graphene.Union):
|
||||
return PowerPortType
|
||||
if type(instance) is RearPort:
|
||||
return RearPortType
|
||||
|
||||
|
||||
class ConnectedEndpointType(graphene.Union):
|
||||
class Meta:
|
||||
types = (
|
||||
CircuitTerminationType,
|
||||
ConsolePortType,
|
||||
ConsoleServerPortType,
|
||||
FrontPortType,
|
||||
InterfaceType,
|
||||
PowerFeedType,
|
||||
PowerOutletType,
|
||||
PowerPortType,
|
||||
ProviderNetworkType,
|
||||
RearPortType,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) is CircuitTermination:
|
||||
return CircuitTerminationType
|
||||
if type(instance) is ConsolePortType:
|
||||
return ConsolePortType
|
||||
if type(instance) is ConsoleServerPort:
|
||||
return ConsoleServerPortType
|
||||
if type(instance) is FrontPort:
|
||||
return FrontPortType
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) is PowerFeed:
|
||||
return PowerFeedType
|
||||
if type(instance) is PowerOutlet:
|
||||
return PowerOutletType
|
||||
if type(instance) is PowerPort:
|
||||
return PowerPortType
|
||||
if type(instance) is ProviderNetwork:
|
||||
return ProviderNetworkType
|
||||
if type(instance) is RearPort:
|
||||
return RearPortType
|
||||
|
||||
@@ -13,7 +13,7 @@ class CabledObjectMixin:
|
||||
|
||||
|
||||
class PathEndpointMixin:
|
||||
connected_endpoints = graphene.List('dcim.graphql.gfk_mixins.LinkPeerType')
|
||||
connected_endpoints = graphene.List('dcim.graphql.gfk_mixins.ConnectedEndpointType')
|
||||
|
||||
def resolve_connected_endpoints(self, info):
|
||||
# Handle empty values
|
||||
|
||||